mirror of
https://github.com/sinseman44/PyCNC.git
synced 2026-04-19 18:38:14 +00:00
first commit
This commit is contained in:
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
*.pyc
|
||||
__pycache__
|
||||
.iws
|
||||
workspace.xml
|
||||
tasks.xml
|
||||
.idea/dictionaries
|
||||
pycnc.tar.bz2
|
||||
12
.idea/PyCNC.iml
generated
Normal file
12
.idea/PyCNC.iml
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="jdk" jdkName="Python 2.7.13 (/usr/bin/python2.7)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="TestRunnerService">
|
||||
<option name="projectConfiguration" value="Nosetests" />
|
||||
<option name="PROJECT_TEST_RUNNER" value="Nosetests" />
|
||||
</component>
|
||||
</module>
|
||||
15
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
15
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="PyCompatibilityInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="ourVersions">
|
||||
<value>
|
||||
<list size="2">
|
||||
<item index="0" class="java.lang.String" itemvalue="2.7" />
|
||||
<item index="1" class="java.lang.String" itemvalue="3.6" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
7
.idea/misc.xml
generated
Normal file
7
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 2.7.13 (/usr/bin/python2.7)" project-jdk-type="Python SDK" />
|
||||
<component name="PythonCompatibilityInspectionAdvertiser">
|
||||
<option name="version" value="2" />
|
||||
</component>
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/PyCNC.iml" filepath="$PROJECT_DIR$/.idea/PyCNC.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017 Nikolay Khabarov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
59
README.md
Normal file
59
README.md
Normal file
@@ -0,0 +1,59 @@
|
||||
This project bring CNC control for ARM based Linux boards like Raspberry Pi.
|
||||
Typically there is no way to control stepper motors from Linux runtime
|
||||
environment due to the lack of real time GPIO control. Even kernel based
|
||||
modules can not guarantee precise control of pulses for steppers. There is
|
||||
always way out. DMA(Direct Memory Access) is a separated hardware module which
|
||||
provides high precision for GPIO outputs. This module can copy bytes which
|
||||
represent GPIO states from RAM buffer directly to GPIO with some clock based
|
||||
on main chip internal oscillator without using CPU's cores. Using such approach
|
||||
this project generates impulses for moving stepper motors and that is very
|
||||
precise way regardless CPU load and OS time jitter.
|
||||
This approach also allows to use Python language for this project. Typically,
|
||||
Python is not good choice for real time application, but since project just
|
||||
needs to set up DMA buffers and hardware will do the rest, Python become the
|
||||
perfect choice for easy development of this project.
|
||||
|
||||
Video demo - [YouTube video](https://youtu.be/vcedo59raS4)
|
||||
|
||||
#Current command support
|
||||
G0, G1, G4, G20, G21, G28, G90, G91, G92, M2, M3, M5, M30
|
||||
Commands can be easily added, see [gmachine.py](./gmachine.py) file.
|
||||
|
||||
#Config
|
||||
All config is stored in [config.py](./config.py) and contains hardware properties, limitations
|
||||
and pin names for hardware control.
|
||||
|
||||
#Hardware
|
||||
Currently, this project supports Raspberry Pi 1-3. But there is a way to add new boards.
|
||||
See [hal.py](./hal.py) file.
|
||||
|
||||
#Performance notice
|
||||
Pure Python interpreter wouldn't provide great performance for high speed machines.
|
||||
Overspeeding setting causes motors mispulses and probably lose of trajectory. According
|
||||
to my tests, Raspberry Pi 2 can handle axises with 400 pulses on mm with top velocity ~800 mm
|
||||
per min. There is always way out! :) Use JIT Python implementation like PyPy. RPi2 can handle
|
||||
up to 18000 mm per minute on the machine with 80 steps per millimeter motors with PyPy.
|
||||
_Note: Raspbian has outdated PyPy version in repositories(v4.0). Moreover v4.0 has issue with
|
||||
`mmap` module implementation. Use PyPy v5.0+, download it for your OS from
|
||||
[here](https://pypy.org/download.html)._
|
||||
PyPy installation:
|
||||
```bash
|
||||
wget wget https://bitbucket.org/pypy/pypy/downloads/pypy2-v5.7.1-linux-armhf-raspbian.tar.bz2
|
||||
sudo mkdir /opt/pypy
|
||||
sudo tar xvf pypy2-v5.7.1-linux-armhf-raspbian.tar.bz2 --directory /opt/pypy/ --strip-components=1
|
||||
sudo ln -s /opt/pypy/bin/pypy /usr/local/bin/pypy
|
||||
```
|
||||
|
||||
#Dependencies
|
||||
Nothing. Just pure Python code.
|
||||
|
||||
#GCode simulation
|
||||
Just a link to a nice web software for gcode files emulation (very helpful for manual creating of
|
||||
gcode files): [https://nraynaud.github.io/webgcode/](https://nraynaud.github.io/webgcode/)
|
||||
|
||||
#License
|
||||
see [LICENSE](./LICENSE) file.
|
||||
|
||||
#Author
|
||||
Nikolay Khabarov
|
||||
|
||||
35
config.py
Normal file
35
config.py
Normal file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Hardware limitations config
|
||||
STEPPER_PULSE_LINGTH_US = 2
|
||||
STEPPER_MAX_VELOCITY_MM_PER_MIN = 1800 # mm per min
|
||||
STEPPER_MAX_ACCELERATION_MM_PER_S2 = 200 # mm per sec^2
|
||||
STEPPER_PULSES_PER_MM = 400
|
||||
|
||||
TABLE_SIZE_X_MM = 200
|
||||
TABLE_SIZE_Y_MM = 300
|
||||
TABLE_SIZE_Z_MM = 48
|
||||
|
||||
SPINDLE_MAX_RPM = 10000
|
||||
|
||||
# Pins config
|
||||
STEPPER_STEP_PIN_X = 16
|
||||
STEPPER_STEP_PIN_Y = 20
|
||||
STEPPER_STEP_PIN_Z = 21
|
||||
|
||||
STEPPER_DIR_PIN_X = 13
|
||||
STEPPER_DIR_PIN_Y = 19
|
||||
STEPPER_DIR_PIN_Z = 26
|
||||
|
||||
SPINDLE_PWM_PIN = 7
|
||||
|
||||
ENDSTOP_PIN_X = 12
|
||||
ENDSTOP_PIN_Y = 6
|
||||
ENDSTOP_PIN_Z = 5
|
||||
|
||||
# Hardware behavior config
|
||||
# Run command immediately after receiving and stream new pulses, otherwise
|
||||
# buffer will be prepared firstly and then command will run.
|
||||
# Before enabling this feature, please make sure that board performance is
|
||||
# enough for streaming pulses(faster then real time).
|
||||
INSTANT_RUN = True
|
||||
86
coordinates.py
Normal file
86
coordinates.py
Normal file
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import math
|
||||
|
||||
|
||||
class Coordinates(object):
|
||||
""" This object represent machine coordinates.
|
||||
Machine supports 3 axis, so there are X, Y and Z.
|
||||
"""
|
||||
def __init__(self, x, y, z):
|
||||
""" Create object.
|
||||
:param x: x coordinated.
|
||||
:param y: y coordinated.
|
||||
:param z: z coordinated.
|
||||
"""
|
||||
self.x = round(x, 10)
|
||||
self.y = round(y, 10)
|
||||
self.z = round(z, 10)
|
||||
|
||||
def is_zero(self):
|
||||
""" Check if all coordinates are zero.
|
||||
:return: boolean value.
|
||||
"""
|
||||
return self.x == 0.0 and self.y == 0.0 and self.z == 0.0
|
||||
|
||||
def is_in_aabb(self, p1, p2):
|
||||
""" Check coordinates are in aabb(Axis-Aligned Bounding Box).
|
||||
aabb is specified with two points.
|
||||
:param p1: First point in Coord object.
|
||||
:param p2: Second point in Coord object.
|
||||
:return: boolean value.
|
||||
"""
|
||||
minx, maxx = sorted((p1.x, p2.x))
|
||||
miny, maxy = sorted((p1.y, p2.y))
|
||||
minz, maxz = sorted((p1.z, p2.z))
|
||||
if self.x < minx or self.y < miny or self.z < minz:
|
||||
return False
|
||||
if self.x > maxx or self.y > maxy or self.z > maxz:
|
||||
return False
|
||||
return True
|
||||
|
||||
def length(self):
|
||||
""" Calculate the length of vector.
|
||||
:return: Vector length.
|
||||
"""
|
||||
return math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z)
|
||||
|
||||
def round(self, base):
|
||||
""" Round values to specified base, ie 0.49 with base 0.25 will be 0.5.
|
||||
:param base: Base.
|
||||
:return: New rounded object.
|
||||
"""
|
||||
return Coordinates(round(self.x / base) * base,
|
||||
round(self.y / base) * base,
|
||||
round(self.z / base) * base)
|
||||
|
||||
def find_max(self):
|
||||
""" Find a maximum value of all values.
|
||||
:return: maximum value.
|
||||
"""
|
||||
return max(self.x, self.y, self.z)
|
||||
|
||||
# build in function implementation
|
||||
def __add__(self, other):
|
||||
return Coordinates(self.x + other.x, self.y + other.y, self.z + other.z)
|
||||
|
||||
def __sub__(self, other):
|
||||
return Coordinates(self.x - other.x, self.y - other.y, self.z - other.z)
|
||||
|
||||
def __mul__(self, v):
|
||||
return Coordinates(self.x * v, self.y * v, self.z * v)
|
||||
|
||||
def __div__(self, v):
|
||||
return Coordinates(self.x / v, self.y / v, self.z / v)
|
||||
|
||||
def __truediv__(self, v):
|
||||
return Coordinates(self.x / v, self.y / v, self.z / v)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.x == other.x and self.y == other.y and self.z == other.z
|
||||
|
||||
def __str__(self):
|
||||
return '(' + str(self.x) + ', ' + str(self.y) + ', ' + str(self.z) + ')'
|
||||
|
||||
def __abs__(self):
|
||||
return Coordinates(abs(self.x), abs(self.y), abs(self.z))
|
||||
11
deploy.sh
Executable file
11
deploy.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
PASS=raspberry
|
||||
ADDR=pi@192.168.0.208
|
||||
if [ ! -z $1 ]; then
|
||||
ADDR=$1
|
||||
fi
|
||||
tar -cvjSf $(dirname "$0")/pycnc.tar.bz2 $(dirname "$0")/*.py > /dev/null
|
||||
sshpass -p${PASS} scp $(dirname "$0")/pycnc.tar.bz2 "${ADDR}:~/pycnc"
|
||||
sshpass -p${PASS} ssh -t ${ADDR} "(cd ~/pycnc && tar xvf pycnc.tar.bz2) > /dev/null" &> /dev/null
|
||||
sshpass -p${PASS} ssh -t ${ADDR} "sudo pypy ~/pycnc/main.py"
|
||||
87
gcode.py
Normal file
87
gcode.py
Normal file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import re
|
||||
|
||||
from coordinates import Coordinates
|
||||
|
||||
gpattern = re.compile('([A-Z])([-+]?[0-9.]+)')
|
||||
cleanpattern = re.compile('\s+|\(.*?\)|;.*') # white spaces and comments start with ';' and in '()'
|
||||
|
||||
|
||||
class GCodeException(Exception):
|
||||
""" Exceptions while parsing gcode.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class GCode(object):
|
||||
""" This object represent single line of gcode.
|
||||
Do not create it manually, use parse_line() instead.
|
||||
"""
|
||||
def __init__(self, params):
|
||||
""" Create object.
|
||||
:param params: dict with gcode key-values.
|
||||
"""
|
||||
self.params = params
|
||||
|
||||
def get(self, argname, default=None, multiply=1.0):
|
||||
""" Get value from gcode line.
|
||||
:param argname: Value name.
|
||||
:param default: Default value if value doesn't exist.
|
||||
:param multiply: if value exist, multiply it by this value.
|
||||
:return: Value if exists or default otherwise.
|
||||
"""
|
||||
if argname not in self.params:
|
||||
return default
|
||||
return float(self.params[argname]) * multiply
|
||||
|
||||
def getXYZ(self, default, multiply):
|
||||
""" Get X, Y and Z values as Coord object.
|
||||
:param default: Default values, if any of coords is not specified.
|
||||
:param multiply: If value exist, multiply it by this value.
|
||||
:return: Coord object.
|
||||
"""
|
||||
x = self.get('X', default.x, multiply)
|
||||
y = self.get('Y', default.y, multiply)
|
||||
z = self.get('Z', default.z, multiply)
|
||||
return Coordinates(x, y, z)
|
||||
|
||||
def isXYZ(self):
|
||||
""" Check if at least one of the coordinates is present.
|
||||
:return: Boolean value.
|
||||
"""
|
||||
return 'X' in self.params or 'Y' in self.params or 'Z' in self.params
|
||||
|
||||
def command(self):
|
||||
""" Get value from gcode line.
|
||||
:return: String with command or None if no command specified.
|
||||
"""
|
||||
if 'G' in self.params:
|
||||
return 'G' + self.params['G']
|
||||
if 'M' in self.params:
|
||||
return 'M' + self.params['M']
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def parse_line(line):
|
||||
""" Parse line.
|
||||
:param line: String with gcode line.
|
||||
:return: gcode objects.
|
||||
"""
|
||||
line = line.upper()
|
||||
line = re.sub(cleanpattern, '', line)
|
||||
if len(line) == 0:
|
||||
return None
|
||||
if line[0] == '%':
|
||||
return None
|
||||
m = gpattern.findall(line)
|
||||
if not m:
|
||||
raise GCodeException('gcode not found')
|
||||
if len(''.join(["%s%s" % i for i in m])) != len(line):
|
||||
raise GCodeException('extra characters in line')
|
||||
params = dict(m)
|
||||
if len(params) != len(m):
|
||||
raise GCodeException('duplicated gcode entries')
|
||||
if 'G' in params and 'M' in params:
|
||||
raise GCodeException('g and m command found')
|
||||
return GCode(params)
|
||||
139
gmachine.py
Normal file
139
gmachine.py
Normal file
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import time
|
||||
import logging
|
||||
|
||||
import hal
|
||||
from coordinates import Coordinates
|
||||
from config import *
|
||||
|
||||
|
||||
class GMachineException(Exception):
|
||||
""" Exceptions while processing gcode line.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class GMachine(object):
|
||||
""" Main object which control and keep state of whole machine: steppers,
|
||||
spindle, extruder etc
|
||||
"""
|
||||
def __init__(self):
|
||||
""" Initialization.
|
||||
"""
|
||||
self._position = Coordinates(0.0, 0.0, 0.0)
|
||||
# init variables
|
||||
self._velocity = 0
|
||||
self._spindle_rpm = 0
|
||||
self._pause = 0
|
||||
self._local = None
|
||||
self._convertCoordinates = 0
|
||||
self._absoluteCoordinates = 0
|
||||
self.reset()
|
||||
hal.init()
|
||||
|
||||
def destroy(self):
|
||||
self.home()
|
||||
hal.join()
|
||||
|
||||
def reset(self):
|
||||
""" Reinitialize all program configurable thing.
|
||||
"""
|
||||
self._velocity = 1000
|
||||
self._spindle_rpm = 1000
|
||||
self._pause = 0
|
||||
self._local = Coordinates(0.0, 0.0, 0.0)
|
||||
self._convertCoordinates = 1.0
|
||||
self._absoluteCoordinates = True
|
||||
|
||||
def _spindle(self, spindle_speed):
|
||||
hal.spindle_control(100.0 * spindle_speed / SPINDLE_MAX_RPM)
|
||||
|
||||
def _move(self, delta, velocity):
|
||||
delta = delta.round(1.0 / STEPPER_PULSES_PER_MM)
|
||||
if delta.is_zero():
|
||||
return
|
||||
np = self._position + delta
|
||||
if not np.is_in_aabb(Coordinates(0.0, 0.0, 0.0),
|
||||
Coordinates(TABLE_SIZE_X_MM, TABLE_SIZE_Y_MM, TABLE_SIZE_Z_MM)):
|
||||
raise GMachineException("out of effective area")
|
||||
hal.move_linear(delta, velocity)
|
||||
# save position
|
||||
self._position = np
|
||||
|
||||
def home(self):
|
||||
""" Move head to park position
|
||||
"""
|
||||
d = Coordinates(0, 0, -self._position.z)
|
||||
self._move(d, STEPPER_MAX_VELOCITY_MM_PER_MIN)
|
||||
d = Coordinates(-self._position.x, -self._position.y, 0)
|
||||
self._move(d, STEPPER_MAX_VELOCITY_MM_PER_MIN)
|
||||
|
||||
def do_command(self, gcode):
|
||||
""" Perform action.
|
||||
:param gcode: GCode object which represent one gcode line
|
||||
"""
|
||||
if gcode is None:
|
||||
return
|
||||
logging.debug("got command " + str(gcode.params))
|
||||
# read command
|
||||
c = gcode.command()
|
||||
if c is None and gcode.isXYZ():
|
||||
c = 'G1'
|
||||
# read parameters
|
||||
if self._absoluteCoordinates:
|
||||
coord = gcode.getXYZ(self._position, self._convertCoordinates)
|
||||
coord = coord + self._local
|
||||
delta = coord - self._position
|
||||
else:
|
||||
delta = gcode.getXYZ(Coordinates(0.0, 0.0, 0.0), self._convertCoordinates)
|
||||
coord = self._position + delta
|
||||
velocity = gcode.get('F', self._velocity)
|
||||
spindle_rpm = gcode.get('S', self._spindle_rpm)
|
||||
pause = gcode.get('P', self._pause)
|
||||
# check parameters
|
||||
if velocity <= 0 or velocity > STEPPER_MAX_VELOCITY_MM_PER_MIN:
|
||||
raise GMachineException("bad feed speed")
|
||||
if spindle_rpm < 0 or spindle_rpm > SPINDLE_MAX_RPM:
|
||||
raise GMachineException("bad spindle speed")
|
||||
if pause < 0:
|
||||
raise GMachineException("bad delay")
|
||||
# select command and run it
|
||||
if c == 'G0': # rapid move
|
||||
self._move(delta, STEPPER_MAX_VELOCITY_MM_PER_MIN)
|
||||
elif c == 'G1': # linear interpolation
|
||||
self._move(delta, velocity)
|
||||
elif c == 'G4': # delay in s
|
||||
hal.join()
|
||||
time.sleep(pause)
|
||||
elif c == 'G20': # switch to inches
|
||||
self._convertCoordinates = 25.4
|
||||
elif c == 'G21': # switch to mm
|
||||
self._convertCoordinates = 1.0
|
||||
elif c == 'G28': # home
|
||||
self.home()
|
||||
elif c == 'G90': # switch to absolute coords
|
||||
self._absoluteCoordinates = True
|
||||
elif c == 'G91': # switch to relative coords
|
||||
self._absoluteCoordinates = False
|
||||
elif c == 'G92': # switch to local coords
|
||||
self._local = self._position - \
|
||||
gcode.getXYZ(Coordinates(0.0, 0.0, 0.0), self._convertCoordinates)
|
||||
elif c == 'M3': # spinle on
|
||||
self._spindle(spindle_rpm)
|
||||
elif c == 'M5': # spindle off
|
||||
self._spindle(0)
|
||||
elif c == 'M2' or c == 'M30': # program finish, reset everything.
|
||||
self.reset()
|
||||
elif c == 'M111': # enable debug
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
elif c is None: # command not specified(for example, just F was passed)
|
||||
pass
|
||||
else:
|
||||
raise GMachineException("unknown command")
|
||||
# save parameters on success
|
||||
self._velocity = velocity
|
||||
self._spindle_rpm = spindle_rpm
|
||||
self._pause = pause
|
||||
logging.debug("position {}, {}, {}".format(
|
||||
self._position.x, self._position.y, self._position.z))
|
||||
8
hal.py
Normal file
8
hal.py
Normal file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
try:
|
||||
from hal_rpi import *
|
||||
except ImportError:
|
||||
print("----- Hardware not detected, using virtual environment -----")
|
||||
print("----- Use M111 command to enable more detailed debug -----")
|
||||
from hal_virtual import *
|
||||
154
hal_rpi.py
Normal file
154
hal_rpi.py
Normal file
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import logging
|
||||
import time
|
||||
|
||||
import rpgpio
|
||||
|
||||
from pulses import PulseGeneratorLinear
|
||||
from coordinates import Coordinates
|
||||
from config import *
|
||||
|
||||
# Stepper motors channel for RPIO
|
||||
STEPPER_CHANNEL = 0
|
||||
# Since there is no way to add pulses and then start cycle in RPIO,
|
||||
# use this delay to start adding pulses to cycle. It can be easily
|
||||
# solved by modifying RPIO in a way of adding method to start cycle
|
||||
# explicitly.
|
||||
RPIO_START_DELAY_US = 200000
|
||||
# Since RPIO generate cycles in loop, use this delay to stop RPIO
|
||||
# It can be removed if RPIO would allow to run single shot cycle.
|
||||
RPIO_STOP_DELAY_US = 5000000
|
||||
|
||||
US_IN_SECONDS = 1000000
|
||||
|
||||
gpio = rpgpio.GPIO()
|
||||
dma = rpgpio.DMAGPIO()
|
||||
|
||||
STEP_PIN_MASK_X = 1 << STEPPER_STEP_PIN_X
|
||||
STEP_PIN_MASK_Y = 1 << STEPPER_STEP_PIN_Y
|
||||
STEP_PIN_MASK_Z = 1 << STEPPER_STEP_PIN_Z
|
||||
|
||||
def init():
|
||||
""" Initialize GPIO pins and machine itself, including callibration if
|
||||
needed. Do not return till all procedure is completed.
|
||||
"""
|
||||
gpio.init(STEPPER_STEP_PIN_X, rpgpio.GPIO.MODE_OUTPUT)
|
||||
gpio.init(STEPPER_STEP_PIN_Y, rpgpio.GPIO.MODE_OUTPUT)
|
||||
gpio.init(STEPPER_STEP_PIN_Z, rpgpio.GPIO.MODE_OUTPUT)
|
||||
gpio.init(STEPPER_DIR_PIN_X, rpgpio.GPIO.MODE_OUTPUT)
|
||||
gpio.init(STEPPER_DIR_PIN_Y, rpgpio.GPIO.MODE_OUTPUT)
|
||||
gpio.init(STEPPER_DIR_PIN_Z, rpgpio.GPIO.MODE_OUTPUT)
|
||||
gpio.init(ENDSTOP_PIN_X, rpgpio.GPIO.MODE_INPUT_PULLUP)
|
||||
gpio.init(ENDSTOP_PIN_X, rpgpio.GPIO.MODE_INPUT_PULLUP)
|
||||
gpio.init(ENDSTOP_PIN_X, rpgpio.GPIO.MODE_INPUT_PULLUP)
|
||||
|
||||
# calibration
|
||||
gpio.set(STEPPER_DIR_PIN_X)
|
||||
gpio.set(STEPPER_DIR_PIN_Y)
|
||||
gpio.set(STEPPER_DIR_PIN_Z)
|
||||
pins = STEP_PIN_MASK_X | STEP_PIN_MASK_Y | STEP_PIN_MASK_Z
|
||||
dma.clear()
|
||||
dma.add_pulse(pins, STEPPER_PULSE_LINGTH_US)
|
||||
while True:
|
||||
if (STEP_PIN_MASK_X & pins) != 0 and gpio.read(ENDSTOP_PIN_X) == 0:
|
||||
pins &= ~STEP_PIN_MASK_X
|
||||
dma.clear()
|
||||
dma.add_pulse(pins, STEPPER_PULSE_LINGTH_US)
|
||||
if (STEP_PIN_MASK_Y & pins) != 0 and gpio.read(ENDSTOP_PIN_Y) == 0:
|
||||
pins &= ~STEP_PIN_MASK_Y
|
||||
dma.clear()
|
||||
dma.add_pulse(pins, STEPPER_PULSE_LINGTH_US)
|
||||
if (STEP_PIN_MASK_Z & pins) != 0 and gpio.read(ENDSTOP_PIN_Z) == 0:
|
||||
pins &= ~STEP_PIN_MASK_Z
|
||||
dma.clear()
|
||||
dma.add_pulse(pins, STEPPER_PULSE_LINGTH_US)
|
||||
if pins == 0:
|
||||
break
|
||||
dma.run(False)
|
||||
# limit velocity at ~10% of top velocity
|
||||
time.sleep((1 / 0.10) / (STEPPER_MAX_VELOCITY_MM_PER_MIN
|
||||
/ 60 * STEPPER_PULSES_PER_MM))
|
||||
|
||||
|
||||
def spindle_control(percent):
|
||||
""" Spindle control implementation.
|
||||
:param percent: Spindle speed in percent.
|
||||
"""
|
||||
# TODO spindle control.
|
||||
logging.info("TODO spindle control: {}%".format(percent))
|
||||
|
||||
|
||||
def move_linear(delta, velocity):
|
||||
""" Move head to specified position
|
||||
:param delta: Coordinated object, delta position in mm
|
||||
:param velocity: velocity in mm per min
|
||||
"""
|
||||
logging.info("move {} with velocity {}".format(delta, velocity))
|
||||
# initialize generator
|
||||
generator = PulseGeneratorLinear(delta, velocity)
|
||||
# wait if previous command still works
|
||||
while dma.is_active():
|
||||
time.sleep(0.001)
|
||||
|
||||
# set direction pins
|
||||
if delta.x > 0.0:
|
||||
gpio.clear(STEPPER_DIR_PIN_X)
|
||||
else:
|
||||
gpio.set(STEPPER_DIR_PIN_X)
|
||||
if delta.y > 0.0:
|
||||
gpio.clear(STEPPER_DIR_PIN_Y)
|
||||
else:
|
||||
gpio.set(STEPPER_DIR_PIN_Y)
|
||||
if delta.z > 0.0:
|
||||
gpio.clear(STEPPER_DIR_PIN_Z)
|
||||
else:
|
||||
gpio.set(STEPPER_DIR_PIN_Z)
|
||||
|
||||
# prepare and run dma
|
||||
dma.clear()
|
||||
prev = 0
|
||||
is_ran = False
|
||||
st = time.time()
|
||||
for tx, ty, tz in generator:
|
||||
pins = 0
|
||||
k = int(round(min(x for x in (tx, ty, tz) if x is not None)
|
||||
* US_IN_SECONDS))
|
||||
if tx is not None:
|
||||
pins |= STEP_PIN_MASK_X
|
||||
if ty is not None:
|
||||
pins |= STEP_PIN_MASK_Y
|
||||
if tz is not None:
|
||||
pins |= STEP_PIN_MASK_Z
|
||||
if k - prev > 0:
|
||||
dma.add_delay(k - prev)
|
||||
dma.add_pulse(pins, STEPPER_PULSE_LINGTH_US)
|
||||
# TODO not a precise way! pulses will set in queue, instead of crossing
|
||||
# if next pulse start during pulse length. Though it almost doesn't
|
||||
# matter for pulses with 1-2us length.
|
||||
prev = k + STEPPER_PULSE_LINGTH_US
|
||||
# instant run handling
|
||||
if not is_ran and INSTANT_RUN:
|
||||
if k > 500000: # wait at least 500 ms is uploaded
|
||||
if time.time() - st > 0.5:
|
||||
# may be instant run should be canceled here?
|
||||
logging.warn("Buffer preparing for instant run took more "
|
||||
"time then buffer time")
|
||||
dma.run_stream()
|
||||
is_ran = True
|
||||
pt = time.time()
|
||||
if not is_ran:
|
||||
dma.run(False)
|
||||
else:
|
||||
dma.finalize_stream()
|
||||
|
||||
logging.info("prepared in " + str(round(pt - st, 2)) + "s, estimated in "
|
||||
+ str(round(generator.total_time_s(), 2)) + "s")
|
||||
|
||||
|
||||
def join():
|
||||
""" Wait till motors work.
|
||||
"""
|
||||
# wait till dma works
|
||||
while dma.is_active():
|
||||
time.sleep(0.01)
|
||||
91
hal_virtual.py
Normal file
91
hal_virtual.py
Normal file
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import logging
|
||||
import time
|
||||
|
||||
from pulses import PulseGeneratorLinear
|
||||
from config import *
|
||||
from coordinates import Coordinates
|
||||
|
||||
""" This is virtual device class which is very useful for debugging.
|
||||
It checks PulseGenerator with some tests.
|
||||
"""
|
||||
|
||||
def init():
|
||||
""" Initialize GPIO pins and machine itself, including calibration if
|
||||
needed. Do not return till all procedure is completed.
|
||||
"""
|
||||
logging.info("initialize hal")
|
||||
|
||||
|
||||
def spindle_control(percent):
|
||||
""" Spindle control implementation.
|
||||
:param percent: Spindle speed in percent.
|
||||
"""
|
||||
logging.info("spindle control: {}%".format(percent))
|
||||
|
||||
|
||||
def move_linear(delta, velocity):
|
||||
""" Move head to specified position
|
||||
:param delta: Coordinated object, delta position in mm
|
||||
:param velocity: velocity in mm per min
|
||||
"""
|
||||
logging.info("move {} with velocity {}".format(delta, velocity))
|
||||
ix = iy = iz = 0
|
||||
generator = PulseGeneratorLinear(delta, velocity)
|
||||
lx, ly, lz = None, None, None
|
||||
dx, dy, dz = 0, 0, 0
|
||||
mx, my, mz = 0, 0, 0
|
||||
st = time.time()
|
||||
for tx, ty, tz in generator:
|
||||
if tx is not None:
|
||||
if tx > mx:
|
||||
mx = tx
|
||||
tx = int(round(tx * 1000000))
|
||||
ix += 1
|
||||
if lx is not None:
|
||||
dx = tx - lx
|
||||
assert dx > 0, "negative or zero time delta detected for x"
|
||||
lx = tx
|
||||
else:
|
||||
dx = None
|
||||
if ty is not None:
|
||||
if ty > my:
|
||||
my = ty
|
||||
ty = int(round(ty * 1000000))
|
||||
iy += 1
|
||||
if ly is not None:
|
||||
dy = ty - ly
|
||||
assert dy > 0, "negative or zero time delta detected for y"
|
||||
ly = ty
|
||||
else:
|
||||
dy = None
|
||||
if tz is not None:
|
||||
if tz > mz:
|
||||
mz = tz
|
||||
tz = int(round(tz * 1000000))
|
||||
iz += 1
|
||||
if lz is not None:
|
||||
dz = tz - lz
|
||||
assert dz > 0, "negative or zero time delta detected for z"
|
||||
lz = tz
|
||||
else:
|
||||
dz = None
|
||||
# very verbose, uncomment on demand
|
||||
# logging.debug("Iteration {} is {} {} {}".format(max(ix, iy, iz), tx, ty, tz))
|
||||
f = list(x for x in (tx, ty, tz) if x is not None)
|
||||
assert f.count(f[0]) == len(f), "fast forwarded pulse detected"
|
||||
pt = time.time()
|
||||
assert ix / STEPPER_PULSES_PER_MM == abs(delta.x), "x wrong number of pulses"
|
||||
assert iy / STEPPER_PULSES_PER_MM == abs(delta.y), "y wrong number of pulses"
|
||||
assert iz / STEPPER_PULSES_PER_MM == abs(delta.z), "z wrong number of pulses"
|
||||
assert max(mx, my, mz) <= generator.total_time_s(), "interpolation time or pulses wrong"
|
||||
logging.debug("Did {}, {}, {} iterations".format(ix, iy, iz))
|
||||
logging.info("prepared in " + str(round(pt - st, 2)) \
|
||||
+ "s, estimated " + str(round(generator.total_time_s(), 2)) + "s")
|
||||
|
||||
|
||||
def join():
|
||||
""" Wait till motors work.
|
||||
"""
|
||||
logging.info("hal join()")
|
||||
58
main.py
Executable file
58
main.py
Executable file
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import sys
|
||||
import readline
|
||||
import logging
|
||||
logging.basicConfig(level=logging.CRITICAL,
|
||||
format='[%(levelname)s] %(message)s')
|
||||
|
||||
from gcode import GCode, GCodeException
|
||||
from gmachine import GMachine, GMachineException
|
||||
|
||||
try: # python3 compatibility
|
||||
type(raw_input)
|
||||
except NameError:
|
||||
raw_input = input
|
||||
|
||||
machine = GMachine()
|
||||
|
||||
|
||||
def do_line(line):
|
||||
try:
|
||||
g = GCode.parse_line(line)
|
||||
machine.do_command(g)
|
||||
except (GCodeException, GMachineException) as e:
|
||||
print('ERROR ' + str(e))
|
||||
return False
|
||||
print('OK')
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
if len(sys.argv) > 1:
|
||||
# Read file with gcode
|
||||
with open(sys.argv[1], 'r') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
print('> ' + line)
|
||||
if not do_line(line):
|
||||
break
|
||||
else:
|
||||
# Main loop for interactive shell
|
||||
# Use stdin/stdout, additional interfaces like
|
||||
# UART, Socket or any other can be added.
|
||||
print("*************** Welcome to PyCNC! ***************")
|
||||
while True:
|
||||
line = raw_input('> ')
|
||||
if line == 'quit' or line == 'exit':
|
||||
break
|
||||
do_line(line)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
print("\r\nExiting...")
|
||||
machine.destroy()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
227
pulses.py
Normal file
227
pulses.py
Normal file
@@ -0,0 +1,227 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import division
|
||||
import math
|
||||
import logging
|
||||
|
||||
from config import *
|
||||
from coordinates import Coordinates
|
||||
|
||||
SECONDS_IN_MINUTE = 60
|
||||
|
||||
|
||||
class PulseGenerator(object):
|
||||
""" Stepper motors pulses generator.
|
||||
It generates time for each pulses for specified path as accelerated
|
||||
movement for specified velocity, then moves linearly and then braking
|
||||
with the same acceleration.
|
||||
Internally this class treat movement as uniform movement and then
|
||||
translate timings to accelerated movements. To do so, it base on
|
||||
formulas for distance of uniform movement and accelerated move.
|
||||
S = V * Ta = a * Tu^2 / 2
|
||||
where Ta - time for accelerated and Tu for uniform movement.
|
||||
Velocity will never be more then Vmax - maximum velocity of all axises.
|
||||
At the point of maximum velocity we change accelerated movement to
|
||||
uniform, so we can translate time for accelerated movement with this
|
||||
formula:
|
||||
Ta(Tu) = a * Tu^2 / Vmax / 2
|
||||
Now we need just to calculate how much time will accelerate and
|
||||
brake will take and recalculate time for them. Linear part will be as
|
||||
is. Since maximum velocity and acceleration is always the same, there
|
||||
is the ACCELERATION_FACTOR_PER_SEC variable.
|
||||
In the same way round or other interpolation can be implemented based on
|
||||
this class.
|
||||
Note: round interpolation would require direction change during movement.
|
||||
It's not implemented yet.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
""" Create object. Do not create directly this object, inherit this class
|
||||
and implement interpolation function and related methods.
|
||||
All child have to call this method ( super().__init__() ).
|
||||
"""
|
||||
self._iteration_x = 0
|
||||
self._iteration_y = 0
|
||||
self._iteration_z = 0
|
||||
self._acceleration_time_s = 0.0
|
||||
self._linear_time_s = 0.0
|
||||
self._2Vmax_per_a = 0.0
|
||||
|
||||
def _get_movement_parameters(self):
|
||||
""" Get for interpolation. This method have to be reimplemented
|
||||
in parent classes and should calculate 3 parameters.
|
||||
:return: Tuple of three values:
|
||||
acceleration_time_s: time for accelerating and breaking motors
|
||||
during movement
|
||||
linear_time_s: time for uniform movement, it is total movement
|
||||
time minus acceleration and braking time
|
||||
max_axis_velocity_mm_per_sec: maximum velocity of any of axis during
|
||||
movement. Even if whole movement is
|
||||
accelerated, this value should be
|
||||
calculated as top velocity.
|
||||
"""
|
||||
raise NotImplemented
|
||||
|
||||
def _interpolation_function(self, pulse_number):
|
||||
""" Get function for interpolation path. This function should returned
|
||||
values as it is uniform movement. There is only one trick, function
|
||||
must be expressed in terms of position, i.e. t = S / V for linear,
|
||||
where S - distance would be increment on motor minimum step.
|
||||
:param pulse_number: number of pulse.
|
||||
:return: time for each axis or None if movement for axis is finished.
|
||||
"""
|
||||
raise NotImplemented
|
||||
|
||||
def __iter__(self):
|
||||
""" Get iterator.
|
||||
:return: iterable object.
|
||||
"""
|
||||
self._acceleration_time_s, self._linear_time_s, \
|
||||
max_axis_velocity_mm_per_sec = self._get_movement_parameters()
|
||||
# helper variable
|
||||
self._2Vmax_per_a = 2.0 * max_axis_velocity_mm_per_sec \
|
||||
/ STEPPER_MAX_ACCELERATION_MM_PER_S2
|
||||
self._iteration_x = 0
|
||||
self._iteration_y = 0
|
||||
self._iteration_z = 0
|
||||
logging.debug(', '.join("%s: %s" % i for i in vars(self).items()))
|
||||
return self
|
||||
|
||||
def _to_accelerated_time(self, pt_s):
|
||||
""" Internal function to translate uniform movement time to time for
|
||||
accelerated movement.
|
||||
:param pt_s: pseudo time of uniform movement.
|
||||
:return: time for each axis or None if movement for axis is finished.
|
||||
"""
|
||||
# acceleration
|
||||
# S = Tpseudo * Vmax = a * t^2 / 2
|
||||
t = math.sqrt(pt_s * self._2Vmax_per_a)
|
||||
if t <= self._acceleration_time_s:
|
||||
return t
|
||||
|
||||
# linear
|
||||
# pseudo acceleration time Tpseudo = t^2 / ACCELERATION_FACTOR_PER_SEC
|
||||
t = self._acceleration_time_s + pt_s - (self._acceleration_time_s ** 2
|
||||
/ self._2Vmax_per_a)
|
||||
# pseudo breaking time
|
||||
bt = t - self._acceleration_time_s - self._linear_time_s
|
||||
if bt <= 0:
|
||||
return t
|
||||
|
||||
# braking
|
||||
# Vmax * Tpseudo = Vlinear * t - a * t^2 / 2
|
||||
# V on start braking is Vlinear = Taccel * a = Tbreaking * a
|
||||
# Vmax * Tpseudo = Tbreaking * a * t - a * t^2 / 2
|
||||
return 2.0 * self._acceleration_time_s + self._linear_time_s \
|
||||
- math.sqrt(self._acceleration_time_s ** 2 - self._2Vmax_per_a * bt)
|
||||
|
||||
def __next__(self):
|
||||
# for python3
|
||||
return self.next()
|
||||
|
||||
def next(self):
|
||||
""" Iterate pulses.
|
||||
:return: Tuple of three values for each axis which represent time for
|
||||
the next pulse. If there is no pulses left None will be
|
||||
returned.
|
||||
"""
|
||||
tx, ty, tz = self._interpolation_function(self._iteration_x,
|
||||
self._iteration_y,
|
||||
self._iteration_z)
|
||||
# check condition to stop
|
||||
if tx is None and ty is None and tz is None:
|
||||
raise StopIteration
|
||||
|
||||
# convert to real time
|
||||
m = min(x for x in (tx, ty, tz) if x is not None)
|
||||
am = self._to_accelerated_time(m)
|
||||
# sort pulses in time
|
||||
if tx is not None:
|
||||
if tx > m:
|
||||
tx = None
|
||||
else:
|
||||
tx = am
|
||||
self._iteration_x += 1
|
||||
if ty is not None:
|
||||
if ty > m:
|
||||
ty = None
|
||||
else:
|
||||
ty = am
|
||||
self._iteration_y += 1
|
||||
if tz is not None:
|
||||
if tz > m:
|
||||
tz = None
|
||||
else:
|
||||
tz = am
|
||||
self._iteration_z += 1
|
||||
|
||||
return tx, ty, tz
|
||||
|
||||
def total_time_s(self):
|
||||
""" Get total time for movement.
|
||||
:return: time in seconds.
|
||||
"""
|
||||
acceleration_time_s, linear_time_s, _ = self._get_movement_parameters()
|
||||
return acceleration_time_s * 2.0 + linear_time_s
|
||||
|
||||
|
||||
class PulseGeneratorLinear(PulseGenerator):
|
||||
def __init__(self, delta_mm, velocity_mm_per_min):
|
||||
""" Create pulse generator for linear interpolation.
|
||||
:param delta_mm: movement distance of each axis.
|
||||
:param velocity_mm_per_min: desired velocity.
|
||||
"""
|
||||
super(PulseGeneratorLinear, self).__init__()
|
||||
# this class doesn't care about direction
|
||||
self._distance_mm = abs(delta_mm)
|
||||
# velocity of each axis
|
||||
distance_xyz_mm = self._distance_mm.length()
|
||||
self.max_velocity_mm_per_sec = self._distance_mm * (
|
||||
velocity_mm_per_min / SECONDS_IN_MINUTE / distance_xyz_mm)
|
||||
# acceleration time
|
||||
self.acceleration_time_s = self.max_velocity_mm_per_sec.find_max() \
|
||||
/ STEPPER_MAX_ACCELERATION_MM_PER_S2
|
||||
# check if there is enough space to accelerate and brake, adjust time
|
||||
# S = a * t^2 / 2
|
||||
if STEPPER_MAX_ACCELERATION_MM_PER_S2 * self.acceleration_time_s ** 2 \
|
||||
> distance_xyz_mm:
|
||||
self.acceleration_time_s = math.sqrt(distance_xyz_mm /
|
||||
STEPPER_MAX_ACCELERATION_MM_PER_S2)
|
||||
self.linear_time_s = 0.0
|
||||
# V = a * t -> V = 2 * S / t, take half of total distance for acceleration and braking
|
||||
self.max_velocity_mm_per_sec = self._distance_mm / self.acceleration_time_s
|
||||
else:
|
||||
# calculate linear time
|
||||
linear_distance_mm = distance_xyz_mm\
|
||||
- self.acceleration_time_s ** 2 \
|
||||
* STEPPER_MAX_ACCELERATION_MM_PER_S2
|
||||
self.linear_time_s = linear_distance_mm \
|
||||
/ self.max_velocity_mm_per_sec.length()
|
||||
|
||||
def _get_movement_parameters(self):
|
||||
""" Return movement parameters, see super class for details.
|
||||
"""
|
||||
return self.acceleration_time_s, \
|
||||
self.linear_time_s, \
|
||||
self.max_velocity_mm_per_sec.find_max()
|
||||
|
||||
def __linear(self, position_mm, distance_mm, velocity_mm_per_sec):
|
||||
""" Helper function for linear movement.
|
||||
"""
|
||||
# check if need to calculate for this axis
|
||||
if distance_mm == 0.0 or position_mm >= distance_mm:
|
||||
return None
|
||||
# Linear movement, S = V * t -> t = S / V
|
||||
return position_mm / velocity_mm_per_sec
|
||||
|
||||
def _interpolation_function(self, ix, iy, iz):
|
||||
""" Calculate interpolation values for linear movement, see super class
|
||||
for details.
|
||||
"""
|
||||
t_x = self.__linear(ix / STEPPER_PULSES_PER_MM, self._distance_mm.x,
|
||||
self.max_velocity_mm_per_sec.x)
|
||||
t_y = self.__linear(iy / STEPPER_PULSES_PER_MM, self._distance_mm.y,
|
||||
self.max_velocity_mm_per_sec.y)
|
||||
t_z = self.__linear(iz / STEPPER_PULSES_PER_MM, self._distance_mm.z,
|
||||
self.max_velocity_mm_per_sec.z)
|
||||
return t_x, t_y, t_z
|
||||
290
rpgpio.py
Executable file
290
rpgpio.py
Executable file
@@ -0,0 +1,290 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from rpgpio_private import *
|
||||
|
||||
import time
|
||||
import logging
|
||||
import sys
|
||||
import struct
|
||||
|
||||
|
||||
|
||||
class GPIO(object):
|
||||
MODE_OUTPUT = 1
|
||||
MODE_INPUT_NOPULL = 2
|
||||
MODE_INPUT_PULLUP = 3
|
||||
MODE_INPUT_PULLDOWN = 4
|
||||
|
||||
def __init__(self):
|
||||
""" Create object which can control GPIO.
|
||||
This class writes directly to CPU registers and doesn't use any libs
|
||||
or kernel modules.
|
||||
"""
|
||||
self._mem = PhysicalMemory(PERI_BASE + GPIO_REGISTER_BASE)
|
||||
|
||||
def _pullupdn(self, pin, mode):
|
||||
p = self._mem.read_int(GPIO_PULLUPDN_OFFSET)
|
||||
p &= ~3
|
||||
if mode == self.MODE_INPUT_PULLUP:
|
||||
p |= 2
|
||||
elif mode == self.MODE_INPUT_PULLDOWN:
|
||||
p |= 1
|
||||
self._mem.write_int(GPIO_PULLUPDN_OFFSET, p)
|
||||
addr = 4 * int(pin / 32) + GPIO_PULLUPDNCLK_OFFSET
|
||||
self._mem.write_int(addr, 1 << (pin % 32))
|
||||
p = self._mem.read_int(GPIO_PULLUPDN_OFFSET)
|
||||
p &= ~3
|
||||
self._mem.write_int(GPIO_PULLUPDN_OFFSET, p)
|
||||
self._mem.write_int(addr, 0)
|
||||
|
||||
def init(self, pin, mode):
|
||||
""" Initialize or re-initialize GPIO pin.
|
||||
:param pin: pin number.
|
||||
:param mode: one of MODE_* variables in this class.
|
||||
"""
|
||||
addr = 4 * int(pin / 10) + GPIO_FSEL_OFFSET
|
||||
v = self._mem.read_int(addr)
|
||||
v &= ~(7 << ((pin % 10) * 3)) # input value
|
||||
if mode == self.MODE_OUTPUT:
|
||||
v |= (1 << ((pin % 10) * 3)) # output value, base on input
|
||||
self._mem.write_int(addr, v)
|
||||
else:
|
||||
self._mem.write_int(addr, v)
|
||||
self._pullupdn(pin, mode)
|
||||
|
||||
def set(self, pin):
|
||||
""" Set pin to HIGH state.
|
||||
:param pin: pin number.
|
||||
"""
|
||||
addr = 4 * int(pin / 32) + GPIO_SET_OFFSET
|
||||
self._mem.write_int(addr, 1 << (pin % 32))
|
||||
|
||||
def clear(self, pin):
|
||||
""" Set pin to LOW state.
|
||||
:param pin: pin number.
|
||||
"""
|
||||
addr = 4 * int(pin / 32) + GPIO_CLEAR_OFFSET
|
||||
self._mem.write_int(addr, 1 << (pin % 32))
|
||||
|
||||
def read(self, pin):
|
||||
""" Read pin current value.
|
||||
:param pin: pin number.
|
||||
:return: integer value 0 or 1.
|
||||
"""
|
||||
addr = 4 * int(pin / 32) + GPIO_INPUT_OFFSET
|
||||
v = self._mem.read_int(addr)
|
||||
v &= 1 << (pin % 32)
|
||||
if v == 0:
|
||||
return 0
|
||||
return 1
|
||||
|
||||
|
||||
class DMAGPIO(object):
|
||||
_DMA_CONTROL_BLOCK_SIZE = 32
|
||||
_DMA_CHANNEL = 5
|
||||
|
||||
def __init__(self):
|
||||
""" Create object which control GPIO pins via DMA(Direct Memory
|
||||
Access).
|
||||
This object allows to add arbitrary sequence of pulses to any GPIO
|
||||
outputs and run this sequence in background without using CPU since
|
||||
DMA is a separated hardware module.
|
||||
Note: keep this object out of garbage collector until it stops,
|
||||
otherwise memory will be unlocked and it could be overwritten by
|
||||
operating system.
|
||||
"""
|
||||
# allocate buffer for control blocks, always 32 MB
|
||||
self._physmem = CMAPhysicalMemory(32 * 1024 * 1024)
|
||||
self.__current_address = 0
|
||||
|
||||
# prepare dma registers memory map
|
||||
self._dma = PhysicalMemory(PERI_BASE + DMA_BASE)
|
||||
self._pwm = PhysicalMemory(PERI_BASE + PWM_BASE)
|
||||
self._clock = PhysicalMemory(PERI_BASE + CM_BASE)
|
||||
|
||||
# pre calculated variables for control blocks
|
||||
self._delay_info = DMA_TI_NO_WIDE_BURSTS | DMA_SRC_IGNORE \
|
||||
| DMA_TI_PER_MAP(DMA_TI_PER_MAP_PWM) \
|
||||
| DMA_TI_DEST_DREQ
|
||||
self._delay_destination = PHYSICAL_PWM_BUS + 0x18
|
||||
self._delay_stride = 0
|
||||
|
||||
self._pulse_info = DMA_TI_NO_WIDE_BURSTS | DMA_TI_TDMODE \
|
||||
| DMA_TI_WAIT_RESP
|
||||
self._pulse_destination = PHYSICAL_GPIO_BUS + GPIO_SET_OFFSET
|
||||
# YLENGTH is transfers count and XLENGTH size of each transfer
|
||||
self._pulse_length = DMA_TI_TXFR_LEN_YLENGTH(2) \
|
||||
| DMA_TI_TXFR_LEN_XLENGTH(4)
|
||||
self._pulse_stride = DMA_TI_STRIDE_D_STRIDE(12) \
|
||||
| DMA_TI_STRIDE_S_STRIDE(4)
|
||||
|
||||
def add_pulse(self, pins_mask, length_us):
|
||||
""" Add single pulse at the current position.
|
||||
:param pins_mask: bitwise mask of GPIO pins to trigger. Only for first 32 pins.
|
||||
:param length_us: length in us.
|
||||
"""
|
||||
next_cb = self.__current_address + 3 * self._DMA_CONTROL_BLOCK_SIZE
|
||||
if next_cb > self._physmem.get_size():
|
||||
raise MemoryError("Out of allocated memory.")
|
||||
next3 = next_cb + self._physmem.get_bus_address()
|
||||
next2 = next3 - self._DMA_CONTROL_BLOCK_SIZE
|
||||
next1 = next2 - self._DMA_CONTROL_BLOCK_SIZE
|
||||
|
||||
source1 = next1 - 8 # last 8 bytes are padding, use it to store data
|
||||
length2 = 16 * length_us
|
||||
source3 = next3 - 8
|
||||
|
||||
data = (
|
||||
self._pulse_info, source1, self._pulse_destination,
|
||||
self._pulse_length,
|
||||
self._pulse_stride, next1, pins_mask, 0,
|
||||
self._delay_info, 0, self._delay_destination, length2,
|
||||
self._delay_stride, next2, 0, 0,
|
||||
self._pulse_info, source3, self._pulse_destination,
|
||||
self._pulse_length,
|
||||
self._pulse_stride, next3, 0, pins_mask
|
||||
)
|
||||
self._physmem.write(self.__current_address, data)
|
||||
self.__current_address = next_cb
|
||||
|
||||
def add_delay(self, delay_us):
|
||||
""" Add delay at the current position.
|
||||
:param delay_us: delay in us.
|
||||
"""
|
||||
next_cb = self.__current_address + self._DMA_CONTROL_BLOCK_SIZE
|
||||
if next_cb > self._physmem.get_size():
|
||||
raise MemoryError("Out of allocated memory.")
|
||||
next = self._physmem.get_bus_address() + next_cb
|
||||
source = next - 8 # last 8 bytes are padding, use it to store data
|
||||
length = 16 * delay_us
|
||||
data = (
|
||||
self._delay_info, source, self._delay_destination, length,
|
||||
self._delay_stride, next, 0, 0
|
||||
)
|
||||
self._physmem.write(self.__current_address, data)
|
||||
self.__current_address = next_cb
|
||||
|
||||
def finalize_stream(self):
|
||||
""" Mark last added block as the last one.
|
||||
"""
|
||||
self._physmem.write_int(self.__current_address + 20
|
||||
- self._DMA_CONTROL_BLOCK_SIZE, 0)
|
||||
logging.info("DMA took {}MB of memory".
|
||||
format(round(self.__current_address / 1024.0 / 1024.0, 2)))
|
||||
|
||||
def run_stream(self):
|
||||
""" Run DMA module in stream mode, i.e. does'n finalize last block
|
||||
and do not check if there is anything to do.
|
||||
"""
|
||||
# configure PWM hardware module which will clocks DMA
|
||||
self._pwm.write_int(PWM_CTL, 0)
|
||||
self._clock.write_int(CM_CNTL, CM_PASSWORD | CM_SRC_PLLD) # disable
|
||||
while (self._clock.read_int(CM_CNTL) & (1 << 7)) != 0:
|
||||
time.sleep(0.00001) # 10 us, wait until BUSY bit is clear
|
||||
self._clock.write_int(CM_DIV, CM_PASSWORD | CM_DIV_VALUE(50)) # 10MHz
|
||||
self._clock.write_int(CM_CNTL, CM_PASSWORD | CM_SRC_PLLD | CM_ENABLE)
|
||||
self._pwm.write_int(PWM_RNG1, 100)
|
||||
self._pwm.write_int(PWM_DMAC, PWM_DMAC_ENAB
|
||||
| PWM_DMAC_PANIC(15) | PWM_DMAC_DREQ(15))
|
||||
self._pwm.write_int(PWM_CTL, PWM_CTL_CLRF)
|
||||
self._pwm.write_int(PWM_CTL, PWM_CTL_USEF1 | PWM_CTL_PWEN1)
|
||||
# configure DMA
|
||||
addr = 0x100 * self._DMA_CHANNEL
|
||||
cs = self._dma.read_int(addr)
|
||||
cs |= DMA_CS_END
|
||||
self._dma.write_int(addr, cs)
|
||||
self._dma.write_int(addr + 4, self._physmem.get_bus_address())
|
||||
cs = DMA_CS_PRIORITY(7) | DMA_CS_PANIC_PRIORITY(7) | DMA_CS_DISDEBUG
|
||||
self._dma.write_int(addr, cs)
|
||||
cs |= DMA_CS_ACTIVE
|
||||
self._dma.write_int(addr, cs)
|
||||
|
||||
def run(self, loop=False):
|
||||
""" Run DMA module and start sending specified pulses.
|
||||
:param loop: If true, run pulse sequence in infinite loop. Otherwise
|
||||
"""
|
||||
if self.__current_address == 0:
|
||||
raise RuntimeError("Nothing was added.")
|
||||
# fix 'next' field in previous control block
|
||||
if loop:
|
||||
self._physmem.write_int(self.__current_address + 20
|
||||
- self._DMA_CONTROL_BLOCK_SIZE,
|
||||
self._physmem.get_bus_address())
|
||||
else:
|
||||
self.finalize_stream()
|
||||
self.run_stream()
|
||||
|
||||
def stop(self):
|
||||
""" Stop any DMA activities.
|
||||
"""
|
||||
self._pwm.write_int(PWM_CTL, 0)
|
||||
addr = 0x100 * self._DMA_CHANNEL
|
||||
cs = self._dma.read_int(addr)
|
||||
cs |= DMA_CS_ABORT
|
||||
self._dma.write_int(addr, cs)
|
||||
cs &= ~DMA_CS_ACTIVE
|
||||
self._dma.write_int(addr, cs)
|
||||
cs |= DMA_CS_RESET
|
||||
self._dma.write_int(addr, cs)
|
||||
|
||||
def is_active(self):
|
||||
""" Check if DMA is working. Method can check if single sent sequence
|
||||
still active.
|
||||
:return: boolean value
|
||||
"""
|
||||
addr = 0x100 * self._DMA_CHANNEL
|
||||
cs = self._dma.read_int(addr)
|
||||
if cs & DMA_CS_ACTIVE == DMA_CS_ACTIVE:
|
||||
return True
|
||||
return False
|
||||
|
||||
def clear(self):
|
||||
""" Remove any specified pulses.
|
||||
"""
|
||||
self.__current_address = 0
|
||||
|
||||
|
||||
# for testing purpose
|
||||
def main():
|
||||
pin = 21
|
||||
g = GPIO()
|
||||
g.init(pin, GPIO.MODE_INPUT_NOPULL)
|
||||
print("nopull " + str(g.read(pin)))
|
||||
g.init(pin, GPIO.MODE_INPUT_PULLDOWN)
|
||||
print("pulldown " + str(g.read(pin)))
|
||||
g.init(pin, GPIO.MODE_INPUT_PULLUP)
|
||||
print("pullup " + str(g.read(pin)))
|
||||
time.sleep(1)
|
||||
g.init(pin, GPIO.MODE_OUTPUT)
|
||||
g.set(pin)
|
||||
print("set " + str(g.read(pin)))
|
||||
time.sleep(1)
|
||||
g.clear(pin)
|
||||
print("clear " + str(g.read(pin)))
|
||||
time.sleep(1)
|
||||
cma = CMAPhysicalMemory(1*1024*1024)
|
||||
print(str(cma.get_size() / 1024 / 1024) + "MB of memory allocated at " \
|
||||
+ hex(cma.get_phys_address()))
|
||||
a = cma.read_int(0)
|
||||
print("was " + hex(a))
|
||||
cma.write_int(0, 0x12345678)
|
||||
a = cma.read_int(0)
|
||||
assert a == 0x12345678, "Memory isn't written or read correctly"
|
||||
print("now " + hex(a))
|
||||
del cma
|
||||
dg = DMAGPIO()
|
||||
dg.add_pulse(1 << pin, 4000)
|
||||
dg.add_delay(12000)
|
||||
dg.run(True)
|
||||
print("dmagpio is started")
|
||||
try:
|
||||
print("press enter to stop...")
|
||||
sys.stdin.readline()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
dg.stop()
|
||||
g.clear(pin)
|
||||
print("dma stopped")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
200
rpgpio_private.py
Normal file
200
rpgpio_private.py
Normal file
@@ -0,0 +1,200 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
import mmap
|
||||
import struct
|
||||
import re
|
||||
import fcntl
|
||||
import array
|
||||
import atexit
|
||||
import ctypes
|
||||
|
||||
# Raspberry Pi registers
|
||||
# https://www.raspberrypi.org/wp-content/uploads/2012/02/BCM2835-ARM-Peripherals.pdf
|
||||
RPI1_PERI_BASE = 0x20000000
|
||||
RPI2_3_PERI_BASE = 0x3F000000
|
||||
# detect board version
|
||||
with open("/proc/cpuinfo", "r") as f:
|
||||
d = f.read()
|
||||
r = re.search("^Revision\s+:\s+(.+)$", d, flags=re.MULTILINE)
|
||||
h = re.search("^Hardware\s+:\s+(.+)$", d, flags=re.MULTILINE)
|
||||
RPI_1_REVISIONS = ['0002', '0003', '0004', '0005', '0006', '0007', '0008',
|
||||
'0009', '000d', '000e', '000f', '0010', '0011', '0012',
|
||||
'0013', '0014', '0015', '900021', '900032']
|
||||
if h is None:
|
||||
raise ImportError("This is not raspberry pi board.")
|
||||
elif r.group(1) in RPI_1_REVISIONS:
|
||||
PERI_BASE = RPI1_PERI_BASE
|
||||
elif "BCM2" in h.group(1):
|
||||
PERI_BASE = RPI2_3_PERI_BASE
|
||||
else:
|
||||
raise ImportError("Unknown board.")
|
||||
PAGE_SIZE = 4096
|
||||
GPIO_REGISTER_BASE = 0x200000
|
||||
GPIO_INPUT_OFFSET = 0x34
|
||||
GPIO_SET_OFFSET = 0x1C
|
||||
GPIO_CLEAR_OFFSET = 0x28
|
||||
GPIO_FSEL_OFFSET = 0x0
|
||||
GPIO_PULLUPDN_OFFSET = 0x94
|
||||
GPIO_PULLUPDNCLK_OFFSET = 0x98
|
||||
PHYSICAL_GPIO_BUS = 0x7E000000 + GPIO_REGISTER_BASE
|
||||
|
||||
# registers and values for DMA
|
||||
DMA_BASE = 0x007000
|
||||
DMA_TI_NO_WIDE_BURSTS = 1 << 26
|
||||
DMA_TI_SRC_INC = 1 << 8
|
||||
DMA_TI_DEST_INC = 1 << 4
|
||||
DMA_SRC_IGNORE = 1 << 11
|
||||
DMA_DEST_IGNORE = 1 << 7
|
||||
DMA_TI_TDMODE = 1 << 1
|
||||
DMA_TI_WAIT_RESP = 1 << 3
|
||||
DMA_TI_SRC_DREQ = 1 << 10
|
||||
DMA_TI_DEST_DREQ = 1 << 6
|
||||
DMA_CS_RESET = 1 << 31
|
||||
DMA_CS_ABORT = 1 << 30
|
||||
DMA_CS_DISDEBUG = 1 << 28
|
||||
DMA_CS_END = 1 << 1
|
||||
DMA_CS_ACTIVE = 1 << 0
|
||||
DMA_TI_PER_MAP_PWM = 5
|
||||
DMA_TI_PER_MAP_PCM = 2
|
||||
|
||||
def DMA_TI_PER_MAP(x):
|
||||
return x << 16
|
||||
|
||||
def DMA_TI_TXFR_LEN_YLENGTH(y):
|
||||
return (y & 0x3fff) << 16
|
||||
|
||||
def DMA_TI_TXFR_LEN_XLENGTH(x):
|
||||
return x & 0xffff
|
||||
|
||||
def DMA_TI_STRIDE_D_STRIDE(x):
|
||||
return (x & 0xffff) << 16
|
||||
|
||||
def DMA_TI_STRIDE_S_STRIDE(x):
|
||||
return x & 0xffff
|
||||
|
||||
def DMA_CS_PRIORITY(x):
|
||||
return (x & 0xf) << 16
|
||||
|
||||
def DMA_CS_PANIC_PRIORITY(x):
|
||||
return (x & 0xf) << 20
|
||||
|
||||
# hardware PWM controller registers
|
||||
PWM_BASE = 0x0020C000
|
||||
PWM_CTL= 0x00
|
||||
PWM_DMAC = 0x08
|
||||
PWM_RNG1 = 0x10
|
||||
PWM_FIFO = 0x18
|
||||
PWM_CTL_MODE1 = 1 << 1
|
||||
PWM_CTL_PWEN1 = 1 << 0
|
||||
PWM_CTL_CLRF = 1 << 6
|
||||
PWM_CTL_USEF1 = 1 << 5
|
||||
PWM_DMAC_ENAB = 1 << 31
|
||||
PHYSICAL_PWM_BUS = 0x7E000000 + PWM_BASE
|
||||
|
||||
def PWM_DMAC_PANIC(x):
|
||||
return x << 8
|
||||
|
||||
def PWM_DMAC_DREQ(x):
|
||||
return x
|
||||
|
||||
# clock manager module
|
||||
CM_BASE = 0x00101000
|
||||
CM_CNTL = 40
|
||||
CM_DIV = 41
|
||||
CM_PASSWORD = 0x5A << 24
|
||||
CM_ENABLE = 1 << 4
|
||||
CM_SRC_OSC = 1 # 19.2 MHz
|
||||
CM_SRC_PLLC = 5 # 1000 MHz
|
||||
CM_SRC_PLLD = 6 # 500 MHz
|
||||
CM_SRC_HDMI = 7 # 216 MHz
|
||||
|
||||
def CM_DIV_VALUE(x):
|
||||
return x << 12
|
||||
|
||||
|
||||
class PhysicalMemory(object):
|
||||
def __init__(self, phys_address, size=PAGE_SIZE):
|
||||
""" Create object which maps physical memory to Python's mmap object.
|
||||
:param phys_address: based address of physical memory
|
||||
"""
|
||||
self._size = size
|
||||
phys_address -= phys_address % PAGE_SIZE
|
||||
fd = self._open_dev("/dev/mem")
|
||||
self._rmap = mmap.mmap(fd, size, flags=mmap.MAP_SHARED,
|
||||
prot=mmap.PROT_READ | mmap.PROT_WRITE,
|
||||
offset=phys_address)
|
||||
atexit.register(self.cleanup)
|
||||
|
||||
def cleanup(self):
|
||||
self._rmap.close()
|
||||
|
||||
@staticmethod
|
||||
def _open_dev(name):
|
||||
fd = os.open(name, os.O_SYNC | os.O_RDWR | os.O_LARGEFILE)
|
||||
if fd < 0:
|
||||
raise IOError("Failed to open " + name)
|
||||
return fd
|
||||
|
||||
@staticmethod
|
||||
def _close_dev(fd):
|
||||
os.close(fd)
|
||||
|
||||
def write_int(self, address, int_value):
|
||||
self._rmap[address:address + 4] = struct.pack("I", int_value)
|
||||
|
||||
def write(self, address, data):
|
||||
self._rmap.seek(address)
|
||||
self._rmap.write(struct.pack(str(len(data)) + "I", *data))
|
||||
|
||||
def read_int(self, address):
|
||||
return struct.unpack("I", self._rmap[address:address + 4])[0]
|
||||
|
||||
def get_size(self):
|
||||
return self._size
|
||||
|
||||
|
||||
class CMAPhysicalMemory(PhysicalMemory):
|
||||
IOCTL_MBOX_PROPERTY = ctypes.c_long(0xc0046400).value
|
||||
def __init__(self, size):
|
||||
""" This class allocates continuous memory with specified size, lock it
|
||||
and provide access to it with Python's mmap. It uses RPi video
|
||||
buffers to allocate it (/dev/vcio).
|
||||
:param size: number of bytes to allocate
|
||||
"""
|
||||
size = (size + PAGE_SIZE - 1) // PAGE_SIZE * PAGE_SIZE
|
||||
self._vcio_fd = self._open_dev("/dev/vcio")
|
||||
self._handle = self._send_data(0x3000c, [size, PAGE_SIZE, 0xC]) # allocate memory
|
||||
if self._handle == 0:
|
||||
raise OSError("No memory to allocate with /dev/vcio")
|
||||
self._busmem = self._send_data(0x3000d, [self._handle]) # lock memory
|
||||
if self._busmem == 0:
|
||||
# memory should be freed in __del__
|
||||
raise OSError("Failed to lock memory with /dev/vcio")
|
||||
# print("allocate {} at {} (bus {})".format(size,
|
||||
# hex(self.get_phys_address()), hex(self.get_bus_address())))
|
||||
super(CMAPhysicalMemory, self).__init__(self.get_phys_address(), size)
|
||||
atexit.register(self.free)
|
||||
|
||||
def free(self):
|
||||
self._send_data(0x3000e, [self._handle]) # unlock memory
|
||||
self._send_data(0x3000f, [self._handle]) # free memory
|
||||
self._close_dev(self._vcio_fd)
|
||||
|
||||
def _send_data(self, request, args):
|
||||
data = array.array('I')
|
||||
data.append(24 + 4 * len(args)) # total size
|
||||
data.append(0) # process request
|
||||
data.append(request) # request id
|
||||
data.append(4 * len(args)) # size of the buffer
|
||||
data.append(4 * len(args)) # size of the data
|
||||
data.extend(args) # arguments
|
||||
data.append(0) # end mark
|
||||
fcntl.ioctl(self._vcio_fd, self.IOCTL_MBOX_PROPERTY, data, True)
|
||||
return data[5]
|
||||
|
||||
def get_bus_address(self):
|
||||
return self._busmem
|
||||
|
||||
def get_phys_address(self):
|
||||
return self._busmem & ~0xc0000000
|
||||
35
tests/rects.gcode
Normal file
35
tests/rects.gcode
Normal file
@@ -0,0 +1,35 @@
|
||||
g21
|
||||
g90
|
||||
; move to start position
|
||||
g1x50y50f1800
|
||||
z20
|
||||
g91
|
||||
; run
|
||||
x100
|
||||
y100
|
||||
x-100
|
||||
y-90
|
||||
x90
|
||||
y80
|
||||
x-80
|
||||
y-70
|
||||
x70
|
||||
y60
|
||||
x-60
|
||||
y-50
|
||||
x50
|
||||
y40
|
||||
x-40
|
||||
y-30
|
||||
x30
|
||||
y20
|
||||
x-20
|
||||
y-10
|
||||
x10
|
||||
y5
|
||||
x-5
|
||||
y-2.5
|
||||
x2.5
|
||||
; homing
|
||||
g28
|
||||
|
||||
10
tests/rpgpio-test.sh
Executable file
10
tests/rpgpio-test.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
PASS=raspberry
|
||||
ADDR=pi@192.168.0.208
|
||||
if [ ! -z $1 ]; then
|
||||
ADDR=$1
|
||||
fi
|
||||
sshpass -p${PASS} scp $(dirname "$0")/../rpgpio_private.py "${ADDR}:~"
|
||||
sshpass -p${PASS} scp $(dirname "$0")/../rpgpio.py "${ADDR}:~"
|
||||
sshpass -p${PASS} ssh -t ${ADDR} "sudo ~/rpgpio.py"
|
||||
28
tests/test-parser.gcode
Normal file
28
tests/test-parser.gcode
Normal file
@@ -0,0 +1,28 @@
|
||||
%
|
||||
g28
|
||||
n1 g1x0y0z0
|
||||
n1 g1 x1 y1 z1 ; check if comments is ok
|
||||
g1(check if inline comments work)x2y2(two times)z2
|
||||
(or on separated line)
|
||||
; or like this
|
||||
m3s04000
|
||||
g4 p0.5
|
||||
m5
|
||||
f500
|
||||
g91
|
||||
g20
|
||||
g0x1
|
||||
g0x1
|
||||
g0x1
|
||||
g0x-1
|
||||
g0x-1
|
||||
g0x-1
|
||||
g21
|
||||
g1y1
|
||||
g1y-1
|
||||
g90
|
||||
g92x100y100z100
|
||||
m111
|
||||
g1x98y98z98
|
||||
(head should be in zero position, and last movent with 500 mm/min velocity)
|
||||
m2
|
||||
Reference in New Issue
Block a user