evil-mad-EggBot/inkscape_driver/eggbot.py

1147 wiersze
37 KiB
Python
Czysty Zwykły widok Historia

'''
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
'''
#TODO: Fix possible timeout when raising/lowering pen, and in manual mode.
#TODO: Clean up serial search code, esp. for Windows
import inkex, cubicsuperpath, simplepath, cspsubdiv
from simpletransform import *
#import pathmodifier
from bezmisc import *
import gettext
import sys
import serial
import time
from math import sqrt
import string
F_DEFAULT_SPEED = 1
N_PEN_DOWN_DELAY = 400 # delay (ms) for the pen to go down before the next move
N_PEN_UP_DELAY = 400 # delay (ms) for the pen to up down before the next move
N_PEN_AXIS_STEPS = 1000 # steps the pen motor can move
N_EGG_AXIS_STEPS = 1000 # steps for the egg motor move in one revolution
N_PEN_UP_POS = 50 # Default pen-up position
N_PEN_DOWN_POS = 40 # Default pen-down position
N_SERVOSPEED = 50 # Default pen-lift speed
N_WALK_DEFAULT = 10 # Default steps for walking stepper motors
N_DEFAULT_LAYER = 1 # Default inkscape layer
'''
if bDebug = True, create an HPGL file to show what is being plotted.
Pen up moves are shown in a different color if bDrawPenUpLines = True.
Try viewing the .hpgl file in a shareware program or create a simple viewer.
'''
bDebug = False
miscDebug = False
bDrawPenUpLines = False
bDryRun = False # write the commands to a text file instead of the serial port
platform = sys.platform.lower()
if platform == 'win32':
DEBUG_OUTPUT_FILE = 'C:/test.hpgl'
DRY_RUN_OUTPUT_FILE = 'C:/dry_run.txt'
MISC_OUTPUT_FILE = 'C:/misc.txt'
#STR_DEFAULT_COM_PORT = 'COM6'
else:
import os #, tty
DEBUG_OUTPUT_FILE = os.getenv( 'HOME' ) + '/test.hpgl'
MISC_OUTPUT_FILE = os.getenv( 'HOME' ) + '/misc.txt'
DRY_RUN_OUTPUT_FILE = os.getenv( 'HOME' ) + '/dry_run.txt'
## if platform == 'darwin':
## ''' There's no good value for OS X '''
## STR_DEFAULT_COM_PORT = '/dev/cu.usbmodem1a21'
## elif platform == 'sunos':
## ''' Untested: YMMV '''
## STR_DEFAULT_COM_PORT = '/dev/term/0'
## else:
## ''' Works fine on Ubuntu; YMMV '''
## STR_DEFAULT_COM_PORT = '/dev/ttyACM0'
'''
Break up a bezier curve into smaller curves, each of which
is approximately a straight line within a given tolerance
(the "smoothness" defined by [flat]).
This is a modified version of cspsubdiv.cspsubdiv(). I rewrote the recursive
call because it caused recursion-depth errors on complicated line segments.
'''
def subdivideCubicPath( sp, flat, i=1 ):
while True:
#m = 0
while True:
if i >= len( sp ):
return
p0 = sp[i - 1][1]
p1 = sp[i - 1][2]
p2 = sp[i][0]
p3 = sp[i][1]
b = ( p0, p1, p2, p3 )
if cspsubdiv.maxdist( b ) > flat:
break
i += 1
one, two = beziersplitatt( b, 0.5 )
sp[i - 1][2] = one[1]
sp[i][0] = two[2]
p = [one[2], one[3], two[1]]
sp[i:1] = [p]
#class EggBot(pathmodifier.PathModifier):
class EggBot( inkex.Effect ):
def __init__( self ):
inkex.Effect.__init__( self )
self.OptionParser.add_option( "--smoothness",
action="store", type="float",
dest="smoothness", default=.2,
help="Smoothness of curves" )
## self.OptionParser.add_option( "--comPort",
## action="store", type="string",
## dest="comport", default=STR_DEFAULT_COM_PORT,
## help="USB COM port to connect eggbot.")
self.OptionParser.add_option( "--startCentered",
action="store", type="inkbool",
dest="startCentered", default=True,
help="Start plot with pen centered in the y-axis." )
self.OptionParser.add_option( "--returnToHome",
action="store", type="inkbool",
dest="returnToHome", default=True,
help="Return to home at end of plot." )
self.OptionParser.add_option( "--wraparound",
action="store", type="inkbool",
dest="wraparound", default=True,
help="Egg (x) axis wraps around-- take shortcuts!" )
self.OptionParser.add_option( "--penUpSpeed",
action="store", type="int",
dest="penUpSpeed", default=F_DEFAULT_SPEED,
help="Speed (step/sec) while pen is up." )
self.OptionParser.add_option( "--penDownSpeed",
action="store", type="int",
dest="penDownSpeed", default=F_DEFAULT_SPEED,
help="Speed (step/sec) while pen is down." )
self.OptionParser.add_option( "--penDownDelay",
action="store", type="int",
dest="penDownDelay", default=N_PEN_DOWN_DELAY,
help="Delay after pen down (msec)." )
self.OptionParser.add_option( "--penUpDelay",
action="store", type="int",
dest="penUpDelay", default=N_PEN_UP_DELAY,
help="Delay after pen up (msec)." )
self.OptionParser.add_option( "--togglePenNow",
action="store", type="inkbool",
dest="togglePenNow", default=False,
help="Toggle the pen up/down on Apply." )
self.OptionParser.add_option( "--tab",
action="store", type="string",
dest="tab", default="controls",
help="The active tab when Apply was pressed" )
self.OptionParser.add_option( "--penUpPosition",
action="store", type="int",
dest="penUpPosition", default=N_PEN_UP_POS,
help="Position of pen when lifted" )
self.OptionParser.add_option( "--ServoSpeed",
action="store", type="int",
dest="ServoSpeed", default=N_SERVOSPEED,
help="Rate of lifting/lowering pen " )
self.OptionParser.add_option( "--penDownPosition",
action="store", type="int",
dest="penDownPosition", default=N_PEN_DOWN_POS,
help="Position of pen when lowered" )
self.OptionParser.add_option( "--layernumber",
action="store", type="int",
dest="layernumber", default=N_DEFAULT_LAYER,
help="Selected layer for multilayer plotting" )
self.OptionParser.add_option( "--manualType",
action="store", type="string",
dest="manualType", default="controls",
help="The active tab when Apply was pressed" )
self.OptionParser.add_option( "--WalkDistance",
action="store", type="int",
dest="WalkDistance", default=N_WALK_DEFAULT,
help="Selected layer for multilayer plotting" )
self.OptionParser.add_option( "--cancelOnly",
action="store", type="inkbool",
dest="cancelOnly", default=False,
help="Cancel plot and return home only." )
self.OptionParser.add_option( "--revPenMotor",
action="store", type="inkbool",
dest="revPenMotor", default=False,
help="Reverse motion of pen motor." )
self.OptionParser.add_option( "--revEggMotor",
action="store", type="inkbool",
dest="revEggMotor", default=False,
help="Reverse motion of egg motor." )
self.bPenIsUp = True
self.virtualPenIsUp = False #Keeps track of pen postion when stepping through plot before resuming
self.fX = None
self.fY = None
self.fPrevX = None
self.fPrevY = None
self.ptFirst = None
self.bStopped = False
self.fSpeed = 1
self.resumeMode = False
self.nodeCount = int( 0 ) #NOTE: python uses 32-bit ints.
self.nodeTarget = int( 0 )
self.pathcount = int( 0 )
self.LayersPlotted = 0
self.svgSerialPort = ''
self.svgLayer = int( 0 )
self.svgNodeCount = int( 0 )
self.svgDataRead = False
self.svgLastPath = int( 0 )
self.svgLastPathNC = int( 0 )
self.svgTotalDeltaX = int( 0 )
self.svgTotalDeltaY = int( 0 )
self.nDeltaX = 0
self.nDeltaY = 0
self.DoubleStepSize = True
try:
import motor1600
except ImportError:
self.DoubleStepSize = False
'''Main entry point: check to see which tab is selected, and act accordingly.'''
def effect( self ): #Pick main course of action, depending on active GUI tab when "Apply" was pressed.
self.svg = self.document.getroot()
self.CheckSVGforEggbotData()
if self.options.tab == '"splash"':
self.allLayers = True
self.plotCurrentLayer = True
self.EggbotOpenSerial()
self.svgNodeCount = 0
self.svgLastPath = 0
strButton = self.doRequest( 'QB\r' ) #Query if button pressed
self.svgLayer = 12345; # indicate that we are plotting all layers.
self.plotToEggBot()
elif self.options.tab == '"resume"':
self.EggbotOpenSerial()
strButton = self.doRequest( 'QB\r' ) #Query if button pressed
self.resumePlotSetup()
if self.resumeMode:
self.plotToEggBot()
elif ( self.options.cancelOnly == True ):
pass
else:
inkex.errormsg( gettext.gettext( "Truly sorry, there does not seem to be any in-progress plot to resume." ) )
elif self.options.tab == '"layers"':
self.allLayers = False
self.plotCurrentLayer = False
self.LayersPlotted = 0
self.svgLastPath = 0
self.EggbotOpenSerial()
strButton = self.doRequest( 'QB\r' ) #Query if button pressed
self.svgNodeCount = 0;
self.svgLayer = self.options.layernumber
self.plotToEggBot()
if ( self.LayersPlotted == 0 ):
inkex.errormsg( gettext.gettext( "Truly sorry, but I did not find any named layers to plot." ) )
elif self.options.tab == '"manual"':
self.EggbotOpenSerial()
self.manualCommand()
elif self.options.tab == '"timing"':
self.EggbotOpenSerial()
if self.serialPort is not None:
self.ServoSetupWrapper()
elif self.options.tab == '"options"':
self.EggbotOpenSerial()
if self.serialPort is not None:
self.ServoSetupWrapper()
if self.options.togglePenNow:
self.doCommand( 'TP\r' ) #Toggle pen
self.svgDataRead = False
self.UpdateSVGEggbotData( self.svg )
self.EggbotCloseSerial()
return
def CheckSVGforEggbotData( self ):
self.svgDataRead = False
self.recursiveEggbotDataScan( self.svg )
if ( self.svgDataRead == False ): #if there is no eggbot data, add some:
eggbotlayer = inkex.etree.SubElement( self.svg, 'eggbot' )
eggbotlayer.set( 'serialport', '' )
eggbotlayer.set( 'layer', str( 0 ) )
eggbotlayer.set( 'node', str( 0 ) )
eggbotlayer.set( 'lastpath', str( 0 ) )
eggbotlayer.set( 'lastpathnc', str( 0 ) )
eggbotlayer.set( 'totaldeltax', str( 0 ) )
eggbotlayer.set( 'totaldeltay', str( 0 ) )
def recursiveEggbotDataScan( self, aNodeList ):
if ( self.svgDataRead != True ):
for node in aNodeList:
if node.tag == 'svg':
self.recursiveEggbotDataScan( node )
elif node.tag == inkex.addNS( 'eggbot', 'svg' ) or node.tag == 'eggbot':
self.svgSerialPort = node.get( 'serialport' )
self.svgLayer = int( node.get( 'layer' ) )
self.svgNodeCount = int( node.get( 'node' ) )
try:
self.svgLastPath = int( node.get( 'lastpath' ) )
self.svgLastPathNC = int( node.get( 'lastpathnc' ) )
self.svgTotalDeltaX = int( node.get( 'totaldeltax' ) )
self.svgTotalDeltaY = int( node.get( 'totaldeltay' ) )
self.svgDataRead = True
except:
node.set( 'lastpath', str( 0 ) )
node.set( 'lastpathnc', str( 0 ) )
node.set( 'totaldeltax', str( 0 ) )
node.set( 'totaldeltay', str( 0 ) )
self.svgDataRead = True
def UpdateSVGEggbotData( self, aNodeList ):
if ( self.svgDataRead != True ):
for node in aNodeList:
if node.tag == 'svg':
self.UpdateSVGEggbotData( node )
elif node.tag == inkex.addNS( 'eggbot', 'svg' ) or node.tag == 'eggbot':
node.set( 'serialport', self.svgSerialPort )
node.set( 'layer', str( self.svgLayer ) )
node.set( 'node', str( self.svgNodeCount ) )
node.set( 'lastpath', str( self.svgLastPath ) )
node.set( 'lastpathnc', str( self.svgLastPathNC ) )
node.set( 'totaldeltax', str( self.svgTotalDeltaX ) )
node.set( 'totaldeltay', str( self.svgTotalDeltaY ) )
self.svgDataRead = True
def resumePlotSetup( self ):
self.LayerFound = False
if ( self.svgLayer < 101 ) and ( self.svgLayer >= 0 ):
self.options.layernumber = self.svgLayer
self.allLayers = False
self.plotCurrentLayer = False
self.LayerFound = True
elif ( self.svgLayer == 12345 ): # Plot all layers
self.allLayers = True
self.plotCurrentLayer = True
self.LayerFound = True
if ( self.LayerFound == True ):
if ( self.svgNodeCount > 0 ):
self.nodeTarget = self.svgNodeCount
self.resumeMode = True
if ( self.options.cancelOnly == True ):
self.resumeMode = False
self.fPrevX = self.svgTotalDeltaX
self.fPrevY = self.svgTotalDeltaY
self.fX = 0
self.fY = 0
self.plotLineAndTime()
self.penUp() #Always end with pen-up
self.svgLayer = 0
self.svgNodeCount = 0
self.svgLastPath = 0
self.svgLastPathNC = 0
self.svgTotalDeltaX = 0
self.svgTotalDeltaY = 0
def manualCommand( self ): #execute commands from the "manual" tab
#self.options.manualType
if self.options.manualType == "none":
return
if self.serialPort is None:
return
self.ServoSetup()
#walks are done at pen-down speed.
#TODO: Add auto-detect if pen is up or down.
if self.options.manualType == "raise-pen":
self.ServoSetupWrapper()
self.penUp()
elif self.options.manualType == "lower-pen":
self.ServoSetupWrapper()
self.penDown()
elif self.options.manualType == "enable-motors":
self.sendEnableMotors()
elif self.options.manualType == "disable-motors":
self.sendDisableMotors()
elif self.options.manualType == "version-check":
strVersion = self.doRequest( 'v\r' )
inkex.errormsg( 'I asked the EBB for its version info, and it replied:\n ' + strVersion )
else: # self.options.manualType is "walk-egg-motor" or "walk-pen-motor":
if self.options.manualType == "walk-egg-motor":
self.nDeltaX = self.options.WalkDistance
self.nDeltaY = 0
elif self.options.manualType == "walk-pen-motor":
self.nDeltaY = self.options.WalkDistance
self.nDeltaX = 0
else:
return
strVersion = self.doRequest( 'QP\r' ) #Query pen position: 1 up, 0 down (followed by OK)
if strVersion[0] == '0':
#inkex.errormsg('Pen is down' )
self.fSpeed = self.options.penDownSpeed
if strVersion[0] == '1':
#inkex.errormsg('Pen is up' )
self.fSpeed = self.options.penUpSpeed
if ( self.options.revPenMotor ):
self.nDeltaY = -1 * self.nDeltaY
if ( self.options.revEggMotor ):
self.nDeltaX = -1 * self.nDeltaX
self.nTime = int( round( 1000.0 / self.fSpeed * distance( self.nDeltaX, self.nDeltaY ) ) )
strOutput = ','.join( ['SM', str( self.nTime ), str( self.nDeltaY ), str( self.nDeltaX )] ) + '\r'
self.doCommand( strOutput )
'''Perform the actual plotting, if selected in the interface:'''
def plotToEggBot( self ): #parse the svg data as a series of line segments and send each segment to be plotted
if self.serialPort is None:
return
self.ServoSetup()
if bDebug:
self.debugOut = open( DEBUG_OUTPUT_FILE, 'w' )
if bDrawPenUpLines:
self.debugOut.write( 'IN;SP1;' )
else:
self.debugOut.write( 'IN;PD;' )
try: # wrap everything in a try so we can for sure close the serial port
#self.recursivelyTraverseSvg(self.document.getroot())
self.recursivelyTraverseSvg( self.svg )
self.penUp() #Always end with pen-up
''' return to home, if returnToHome = True '''
if ( ( self.bStopped == False ) and self.options.returnToHome and ( self.ptFirst != None ) ):
self.fX = self.ptFirst[0]
self.fY = self.ptFirst[1]
#self.penUp()
self.nodeCount = self.nodeTarget # enablesfpx return-to-home only option
self.plotLineAndTime()
#inkex.errormsg('Final node count: ' + str(self.svgNodeCount)) #Node Count - Debug option
if ( self.bStopped == False ):
self.svgLayer = 0
self.svgNodeCount = 0
self.svgLastPath = 0
self.svgLastPathNC = 0
self.svgTotalDeltaX = 0
self.svgTotalDeltaY = 0
finally:
return
## Recursively traverse the svg file to plot out all of the
## paths. The function keeps track of the composite transformation
## that should be applied to each path.
##
## This function handles path, group, line, rect, polyline, polygon,
## circle, ellipse and clone elements. Notable elements not handled
## include text. Unhandled elements should be converted to paths in
## Inkscape.
def recursivelyTraverseSvg( self, aNodeList, matCurrent=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], parent_visibility='visible' ):
for node in aNodeList:
''' Ignore invisible nodes '''
v = node.get( 'visibility', parent_visibility )
if v == 'inherit':
v = parent_visibility
if v == 'hidden' or v == 'collapse':
pass
''' first apply the current matrix transform to this node's tranform '''
matNew = composeTransform( matCurrent, parseTransform( node.get( "transform" ) ) )
if node.tag == inkex.addNS( 'g', 'svg' ) or node.tag == 'g':
self.penUp()
if ( node.get( inkex.addNS( 'groupmode', 'inkscape' ) ) == 'layer' ):
if self.allLayers == False:
#inkex.errormsg('Plotting layer named: ' + node.get(inkex.addNS('label', 'inkscape')))
self.DoWePlotLayer( node.get( inkex.addNS( 'label', 'inkscape' ) ) )
self.recursivelyTraverseSvg( node, matNew, parent_visibility=v )
elif node.tag == inkex.addNS( 'use', 'svg' ) or node.tag == 'use':
refid = node.get( inkex.addNS( 'href', 'xlink' ) )
if refid:
# [1:] to ignore leading '#' in reference
path = '//*[@id="%s"]' % refid[1:]
refnode = node.xpath( path )
if refnode:
x = float( node.get( 'x', '0' ) )
y = float( node.get( 'y', '0' ) )
tran = node.get( 'transform' )
if tran:
tran += ' translate(%f,%f)' % ( x, y )
else:
tran = 'translate(%f,%f)' % ( x, y )
matNew2 = composeTransform( matNew, parseTransform( tran ) )
v = node.get( 'visibility', v )
self.recursivelyTraverseSvg( refnode, matNew2, parent_visibility=v )
else:
pass
else:
pass
elif node.tag == inkex.addNS( 'path', 'svg' ):
self.pathcount += 1
#if we're in resume mode AND self.pathcount < self.svgLastPath, then skip over this path.
#if we're in resume mode and self.pathcount = self.svgLastPath, then start here, and set
# self.nodeCount equal to self.svgLastPathNC
if self.resumeMode and ( self.pathcount == self.svgLastPath ):
self.nodeCount = self.svgLastPathNC
if self.resumeMode and ( self.pathcount < self.svgLastPath ):
pass
else:
self.plotPath( node, matNew )
if ( self.bStopped == False ): #an "index" for resuming plots quickly-- record last complete path
self.svgLastPath += 1
self.svgLastPathNC = self.nodeCount
elif node.tag == inkex.addNS( 'rect', 'svg' ) or node.tag == 'rect':
self.pathcount += 1
#if we're in resume mode AND self.pathcount < self.svgLastPath, then skip over this path.
#if we're in resume mode and self.pathcount = self.svgLastPath, then start here, and set
# self.nodeCount equal to self.svgLastPathNC
if self.resumeMode and ( self.pathcount == self.svgLastPath ):
self.nodeCount = self.svgLastPathNC
if self.resumeMode and ( self.pathcount < self.svgLastPath ):
pass
else:
# Create a path with the outline of the rectangle
newpath = inkex.etree.Element( inkex.addNS( 'path', 'svg' ) )
x = float( node.get( 'x' ) )
y = float( node.get( 'y' ) )
w = float( node.get( 'width' ) )
h = float( node.get( 'height' ) )
s = node.get( 'style' )
if s:
newpath.set( 'style', s )
t = node.get( 'transform' )
if t:
newpath.set( 'transform', t )
a = []
a.append( ['M ', [x, y]] )
a.append( [' l ', [w, 0]] )
a.append( [' l ', [0, h]] )
a.append( [' l ', [-w, 0]] )
a.append( [' Z', []] )
newpath.set( 'd', simplepath.formatPath( a ) )
self.plotPath( newpath, matNew )
elif node.tag == inkex.addNS( 'line', 'svg' ) or node.tag == 'line':
self.pathcount += 1
#if we're in resume mode AND self.pathcount < self.svgLastPath, then skip over this path.
#if we're in resume mode and self.pathcount = self.svgLastPath, then start here, and set
# self.nodeCount equal to self.svgLastPathNC
if self.resumeMode and ( self.pathcount == self.svgLastPath ):
self.nodeCount = self.svgLastPathNC
if self.resumeMode and ( self.pathcount < self.svgLastPath ):
pass
else:
# Create a path to contain the line
newpath = inkex.etree.Element( inkex.addNS( 'path', 'svg' ) )
x1 = float( node.get( 'x1' ) )
y1 = float( node.get( 'y1' ) )
x2 = float( node.get( 'x2' ) )
y2 = float( node.get( 'y2' ) )
s = node.get( 'style' )
if s:
newpath.set( 'style', s )
t = node.get( 'transform' )
if t:
newpath.set( 'transform', t )
a = []
a.append( ['M ', [x1, y1]] )
a.append( [' L ', [x2, y2]] )
newpath.set( 'd', simplepath.formatPath( a ) )
self.plotPath( newpath, matNew )
if ( self.bStopped == False ): #an "index" for resuming plots quickly-- record last complete path
self.svgLastPath += 1
self.svgLastPathNC = self.nodeCount
elif node.tag == inkex.addNS( 'polyline', 'svg' ) or node.tag == 'polyline':
# Ignore polylines with no 'points' attribute
pl = node.get( 'points', '' ).strip()
if pl == '':
pass
self.pathcount += 1
#if we're in resume mode AND self.pathcount < self.svgLastPath, then skip over this path.
#if we're in resume mode and self.pathcount = self.svgLastPath, then start here, and set
# self.nodeCount equal to self.svgLastPathNC
if self.resumeMode and ( self.pathcount == self.svgLastPath ):
self.nodeCount = self.svgLastPathNC
if self.resumeMode and ( self.pathcount < self.svgLastPath ):
pass
else:
pa = pl.split()
d = "".join( ["M " + pa[i] if i == 0 else " L " + pa[i] for i in range( 0, len( pa ) )] )
newpath = inkex.etree.Element( inkex.addNS( 'path', 'svg' ) )
newpath.set( 'd', d );
s = node.get( 'style' )
if s:
newpath.set( 'style', s )
t = node.get( 'transform' )
if t:
newpath.set( 'transform', t )
self.plotPath( newpath, matNew )
if ( self.bStopped == False ): #an "index" for resuming plots quickly-- record last complete path
self.svgLastPath += 1
self.svgLastPathNC = self.nodeCount
elif node.tag == inkex.addNS( 'polygon', 'svg' ) or node.tag == 'polygon':
# Ignore polygons which have no 'points' attributes
pl = node.get( 'points', '' ).strip()
if pl == '':
pass
self.pathcount += 1
#if we're in resume mode AND self.pathcount < self.svgLastPath, then skip over this path.
#if we're in resume mode and self.pathcount = self.svgLastPath, then start here, and set
# self.nodeCount equal to self.svgLastPathNC
if self.resumeMode and ( self.pathcount == self.svgLastPath ):
self.nodeCount = self.svgLastPathNC
if self.resumeMode and ( self.pathcount < self.svgLastPath ):
pass
else:
pa = pl.split()
d = "".join( ["M " + pa[i] if i == 0 else " L " + pa[i] for i in range( 0, len( pa ) )] )
d += " Z"
newpath = inkex.etree.Element( inkex.addNS( 'path', 'svg' ) )
newpath.set( 'd', d );
s = node.get( 'style' )
if s:
newpath.set( 'style', s )
t = node.get( 'transform' )
if t:
newpath.set( 'transform', t )
self.plotPath( newpath, matNew )
if ( self.bStopped == False ): #an "index" for resuming plots quickly-- record last complete path
self.svgLastPath += 1
self.svgLastPathNC = self.nodeCount
elif node.tag == inkex.addNS( 'ellipse', 'svg' ) or \
node.tag == 'ellipse' or \
node.tag == inkex.addNS( 'circle', 'svg' ) or \
node.tag == 'circle':
# Ellipses or circles with a radius attribute of value 0 are ignored
if node.tag == inkex.addNS( 'ellipse', 'svg' ) or node.tag == 'ellipse':
rx = float( node.get( 'rx', '0' ) )
ry = float( node.get( 'ry', '0' ) )
else:
rx = float( node.get( 'r', '0' ) )
ry = rx
if rx == 0 or ry == 0:
pass
self.pathcount += 1
#if we're in resume mode AND self.pathcount < self.svgLastPath, then skip over this path.
#if we're in resume mode and self.pathcount = self.svgLastPath, then start here, and set
# self.nodeCount equal to self.svgLastPathNC
if self.resumeMode and ( self.pathcount == self.svgLastPath ):
self.nodeCount = self.svgLastPathNC
if self.resumeMode and ( self.pathcount < self.svgLastPath ):
pass
else:
cx = float( node.get( 'cx', '0' ) )
cy = float( node.get( 'cy', '0' ) )
x1 = cx - rx
x2 = cx + rx
d = 'M %f,%f ' % ( x1, cy ) + \
'A %f,%f ' % ( rx, ry ) + \
'0 1 0 %f,%f ' % ( x2, cy ) + \
'A %f,%f ' % ( rx, ry ) + \
'0 1 0 %f,%f' % ( x1, cy )
newpath = inkex.etree.Element( inkex.addNS( 'path', 'svg' ) )
newpath.set( 'd', d );
s = node.get( 'style' )
if s:
newpath.set( 'style', s )
t = node.get( 'transform' )
if t:
newpath.set( 'transform', t )
self.plotPath( newpath, matNew )
if ( self.bStopped == False ): #an "index" for resuming plots quickly-- record last complete path
self.svgLastPath += 1
self.svgLastPathNC = self.nodeCount
elif node.tag == inkex.addNS( 'pattern', 'svg' ) or node.tag == 'pattern':
pass
elif node.tag == inkex.addNS( 'metadata', 'svg' ) or node.tag == 'metadata':
pass
elif node.tag == inkex.addNS( 'defs', 'svg' ) or node.tag == 'defs':
pass
elif node.tag == inkex.addNS( 'namedview', 'sodipodi' ) or node.tag == 'namedview':
pass
elif node.tag == inkex.addNS( 'eggbot', 'svg' ) or node.tag == 'eggbot':
pass
elif node.tag == inkex.addNS( 'text', 'svg' ) or node.tag == 'text':
inkex.errormsg( 'Warning: unable to draw text, please convert it to a path first.' )
pass
else:
inkex.errormsg( 'Warning: unable to draw object, please convert it to a path first.' )
pass
#We are only plotting *some* layers. Check to see
# whether or not we're going to plot this one.
#
# First: scan first 4 chars of node id for first non-numeric character,
# and scan the part before that (if any) into a number
#
# Then, see if the number matches the layer.
# self.plotCurrentLayer = False
def DoWePlotLayer( self, strLayerName ):
TempNumString = 'x'
stringPos = 1
CurrentLayerName = string.lstrip( strLayerName ) #remove leading whitespace
#Look at layer name. Sample first character, then first two, and
# so on, until the string ends or the string no longer consists of
# digit characters only.
MaxLength = len( CurrentLayerName )
if MaxLength > 0:
while stringPos <= MaxLength:
if str.isdigit( CurrentLayerName[:stringPos] ):
TempNumString = CurrentLayerName[:stringPos] # Store longest numeric string so far
stringPos = stringPos + 1
else:
break
self.plotCurrentLayer = False #Temporarily assume that we aren't plotting the layer
if ( str.isdigit( TempNumString ) ):
if ( self.svgLayer == int( float( TempNumString ) ) ):
self.plotCurrentLayer = True #We get to plot the layer!
self.LayersPlotted += 1
#Note: this function is only called if we are NOT plotting all layers.
'''
Plot the path while applying the transformation defined
by the matrix [matTransform].
'''
def plotPath( self, path, matTransform ):
''' turn this path into a cubicsuperpath (list of beziers)... '''
d = path.get( 'd' )
if len( simplepath.parsePath( d ) ) == 0:
return
p = cubicsuperpath.parsePath( d )
''' ...and apply the transformation to each point '''
applyTransformToPath( matTransform, p )
'''
p is now a list of lists of cubic beziers [control pt1, control pt2, endpoint]
where the start-point is the last point in the previous segment.
'''
for sp in p:
subdivideCubicPath( sp, self.options.smoothness )
nIndex = 0
for csp in sp:
if self.bStopped:
return
if self.plotCurrentLayer:
if nIndex == 0:
self.penUp()
self.virtualPenIsUp = True
elif nIndex == 1:
self.penDown()
self.virtualPenIsUp = False
nIndex += 1
if ( self.DoubleStepSize == True ):
self.fX = float( csp[1][0] ) / 2.0
self.fY = float( csp[1][1] ) / 2.0
else:
self.fX = float( csp[1][0] )
self.fY = float( csp[1][1] )
''' store home '''
if self.ptFirst is None:
''' if we should start at center, then the first line segment should draw from there '''
if self.options.startCentered:
self.fPrevX = self.fX
#self.fPrevY = N_PEN_AXIS_STEPS / 2.0
if ( self.DoubleStepSize == True ):
self.fPrevY = N_PEN_AXIS_STEPS / 4.0
else:
self.fPrevY = N_PEN_AXIS_STEPS / 2.0
self.ptFirst = ( self.fPrevX, self.fPrevY )
else:
self.ptFirst = ( self.fX, self.fY )
if self.plotCurrentLayer:
self.plotLineAndTime()
self.fPrevX = self.fX
self.fPrevY = self.fY
def sendEnableMotors( self ):
self.doCommand( 'EM,1,1\r' )
def sendDisableMotors( self ):
self.doCommand( 'EM,0,0\r' )
def penUp( self ):
if ( ( self.resumeMode != True ) or ( self.virtualPenIsUp != True ) ):
self.doCommand( 'SP,1\r' )
self.doCommand( 'SM,' + str( self.options.penUpDelay ) + ',0,0\r' ) # pause for pen to go up
self.bPenIsUp = True
self.virtualPenIsUp = True
def penDown( self ):
self.virtualPenIsUp = False # Virtual pen keeps track of state for resuming plotting.
if ( self.resumeMode != True ):
self.doCommand( 'SP,0\r' )
self.doCommand( 'SM,' + str( self.options.penDownDelay ) + ',0,0\r' ) # pause for pen to go down
self.bPenIsUp = False
def ServoSetupWrapper( self ):
self.ServoSetup()
strVersion = self.doRequest( 'QP\r' ) #Query pen position: 1 up, 0 down (followed by OK)
if strVersion[0] == '0':
#inkex.errormsg('Pen is down' )
self.doCommand( 'SP,0\r' ) #Lower Pen
else:
self.doCommand( 'SP,1\r' ) #Raise pen
def ServoSetup( self ):
# Pen position units range from 0% to 100%, which correspond to
# a timing range of 6000 - 30000 in units of 1/(12 MHz).
# 1% corresponds to 20 us, or 240 units of 1/(12 MHz).
intTemp = 240 * ( self.options.penUpPosition + 25 )
self.doCommand( 'SC,4,' + str( intTemp ) + '\r' )
intTemp = 240 * ( self.options.penDownPosition + 25 )
self.doCommand( 'SC,5,' + str( intTemp ) + '\r' )
# Servo speed units are in units of %/second, referring to the
# percentages above. The EBB takes speeds in units of 1/(12 MHz) steps
# per 21 ms. Scaling as above, 1% in 1 second corresponds to
# 240 steps/s, which corresponds to 0.240 steps/ms, which corresponds
# to 5.04 steps/21 ms. Rounding this to 5 steps/21 ms is correct
# to within 1 %.
intTemp = 5 * self.options.ServoSpeed
self.doCommand( 'SC,10,' + str( intTemp ) + '\r' )
#inkex.errormsg('Setting up Servo Motors!')
def stop( self ):
self.bStopped = True
'''
Send commands out the com port as a line segment (dx, dy) and a time (ms) the segment
should take to implement
'''
def plotLineAndTime( self ):
if self.bStopped:
return
if ( self.fPrevX is None ):
return
self.nDeltaX = int( self.fX ) - int( self.fPrevX )
self.nDeltaY = int( self.fY ) - int( self.fPrevY )
if self.bPenIsUp:
self.fSpeed = self.options.penUpSpeed
if ( self.options.wraparound == True ):
if ( self.DoubleStepSize == True ):
if ( self.nDeltaX > 800 ):
self.nDeltaX -= 1600
elif ( self.nDeltaX < -800 ):
self.nDeltaX += 1600
else:
if ( self.nDeltaX > 1600 ):
self.nDeltaX -= 3200
elif ( self.nDeltaX < -1600 ):
self.nDeltaX += 3200
else:
self.fSpeed = self.options.penDownSpeed
if ( distance( self.nDeltaX, self.nDeltaY ) > 0 ):
self.nodeCount += 1
if self.resumeMode:
if ( self.nodeCount > self.nodeTarget ):
self.resumeMode = False
#inkex.errormsg('First node plotted will be number: ' + str(self.nodeCount))
if ( self.virtualPenIsUp != True ):
self.penDown()
self.fSpeed = self.options.penDownSpeed
nTime = int( math.ceil( 1000 / self.fSpeed * distance( self.nDeltaX, self.nDeltaY ) ) )
while ( ( abs( self.nDeltaX ) > 0 ) or ( abs( self.nDeltaY ) > 0 ) ):
if ( nTime > 750 ):
xd = int( round( ( 750.0 * self.nDeltaX ) / nTime ) )
yd = int( round( ( 750.0 * self.nDeltaY ) / nTime ) )
td = int( 750 )
else:
xd = self.nDeltaX
yd = self.nDeltaY
td = nTime
if ( td < 1 ):
td = 1 # don't allow zero-time moves.
if ( self.resumeMode != True ):
if ( self.options.revPenMotor ):
yd2 = yd
else:
yd2 = -yd
if ( self.options.revEggMotor ):
xd2 = -xd
else:
xd2 = xd
strOutput = ','.join( ['SM', str( td ), str( yd2 ), str( xd2 )] ) + '\r'
self.svgTotalDeltaX += xd
self.svgTotalDeltaY += yd
self.doCommand( strOutput )
self.nDeltaX -= xd
self.nDeltaY -= yd
nTime -= td
#self.doCommand('NI\r') #Increment node counter on EBB
strButton = self.doRequest( 'QB\r' ) #Query if button pressed
if strButton[0] == '0':
pass #button not pressed
else:
self.svgNodeCount = self.nodeCount;
inkex.errormsg( 'Plot paused by button press after segment number ' + str( self.nodeCount ) + '.' )
inkex.errormsg( 'Use the "resume" feature to continue.' )
#self.penUp() # Should be redundant...
self.bStopped = True
return
''' note: the pen-motor is first, and it corresponds to the y-axis on-screen '''
def EggbotOpenSerial( self ):
if not bDryRun:
self.serialPort = self.getSerialPort()
else:
self.serialPort = open( DRY_RUN_OUTPUT_FILE, 'w' )
if self.serialPort is None:
inkex.errormsg( gettext.gettext( "Unable to find an Eggbot on any serial port. :(" ) )
def EggbotCloseSerial( self ):
try:
if self.serialPort != None:
self.serialPort.flush()
self.serialPort.close()
if bDebug:
self.debugOut.close()
finally:
self.serialPort = None
return
'''
look at COM1 to COM20 and return a SerialPort object
for the first port with an EBB (eggbot board).
YOU are responsible for closing this serial port!
'''
def testSerialPort( self, strComPort ):
try:
serialPort = serial.Serial( strComPort, timeout=1 ) # 1 second timeout!
serialPort.setRTS() # ??? remove
serialPort.setDTR() # ??? remove
serialPort.flushInput()
serialPort.flushOutput()
time.sleep( 0.1 )
serialPort.write( 'v\r' )
strVersion = serialPort.readline()
if strVersion != None and strVersion.startswith( 'EBB' ):
# do version control here to check the firmware...
return serialPort
serialPort.close()
except serial.SerialException:
pass
return None
def getSerialPort( self ):
if platform == 'win32':
# Before searching, first check to see if the
# last known serial port is still good.
serialPort = self.testSerialPort( self.svgSerialPort )
if serialPort != None:
return serialPort
for i in range( 1, 100 ):
strComPort = 'COM' + str( i )
serialPort = self.testSerialPort( strComPort )
if serialPort != None:
return serialPort
else:
if platform == 'darwin':
strDir = '/dev'
strPrefix = 'cu.usbmodem'
elif platform == 'sunos5':
strDir = '/dev/term'
strPrefix = None
else:
strDir = '/dev'
strPrefix = 'ttyACM'
device_list = os.listdir( strDir )
# Before searching, first check to see if the
# last known serial port is still good.
if self.svgSerialPort[len( strDir ) + 1:] in device_list:
serialPort = self.testSerialPort( self.svgSerialPort )
if serialPort != None:
return serialPort
# Before going through the entire device list, try
# another approach to finding the Eggbot serial port
if platform == 'darwin':
usbdata = os.popen( '/usr/sbin/system_profiler SPUSBDataType' ).read()
match = re.match( r".*EiBotBoard:.*?Location ID: 0x(\w+).*", str( usbdata ), re.M | re.S )
if match != None:
locid = int( match.group( 1 ), 16 )
strComPort = '/dev/cu.usbmodem%x' % ( ( locid >> 16 ) + 1 )
serialPort = self.testSerialPort( strComPort )
if serialPort != None:
self.svgSerialPort = strComPort
return serialPort
for device in device_list:
if strPrefix != None:
if not device.startswith( strPrefix ):
continue
strComPort = strDir + '/' + device
serialPort = self.testSerialPort( strComPort )
if serialPort != None:
self.svgSerialPort = strComPort
return serialPort
return None
def doCommand( self, cmd ):
try:
self.serialPort.write( cmd )
response = self.serialPort.readline()
if ( response != 'OK\r\n' ):
if ( response != '' ):
inkex.errormsg( 'After command ' + cmd + ',' )
inkex.errormsg( 'Received bad response from EBB: ' + str( response ) + '.' )
#inkex.errormsg('BTW:: Node number is ' + str(self.nodeCount) + '.')
else:
inkex.errormsg( 'EBB Serial Timeout.' )
except:
pass
def doRequest( self, cmd ):
response = ''
try:
self.serialPort.write( cmd )
response = self.serialPort.readline()
responseDummy = self.serialPort.readline() #read in extra blank/OK line
except:
inkex.errormsg( gettext.gettext( "Error reading serial data." ) )
return response
'''
Pythagorean theorem!
'''
def distance( x, y ):
return sqrt( x * x + y * y )
e = EggBot()
#e.affect(output=False)
e.affect()