# 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 from bezmisc import * from math import sqrt from simpletransform import * import gettext import simplepath import cspsubdiv import os import serial import string import sys import time import eggbot_scan 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() HOME = os.getenv( 'HOME' ) if platform == 'win32': HOME = os.path.realpath( "C:/" ) # Arguably, this should be %APPDATA% or %TEMP% DEBUG_OUTPUT_FILE = os.path.join( HOME, 'test.hpgl' ) DRY_RUN_OUTPUT_FILE = os.path.join( HOME, 'dry_run.txt' ) MISC_OUTPUT_FILE = os.path.join( HOME, 'misc.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' def subdivideCubicPath( sp, flat, i=1 ): """ 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. """ while True: 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( 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 def effect( self ): '''Main entry point: check to see which tab is selected, and act accordingly.''' 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 unused_button = 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() unused_button = 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() unused_button = 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""" 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 #Query pen position: 1 up, 0 down (followed by OK) strVersion = self.doRequest( 'QP\r' ) 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 ) def plotToEggBot( self ): '''Perform the actual plotting, if selected in the interface:''' #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 def recursivelyTraverseSvg( self, aNodeList, matCurrent=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], parent_visibility='visible' ): """ 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. """ 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 def DoWePlotLayer( self, strLayerName ): """ 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. """ 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. def plotPath( self, path, matTransform ): ''' Plot the path while applying the transformation defined by the matrix [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 def plotLineAndTime( self ): ''' Send commands out the com port as a line segment (dx, dy) and a time (ms) the segment should take to implement ''' 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 def testSerialPort( self, strComPort ): ''' 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! ''' 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 ): # 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 # Try any devices which seem to have EBB boards attached for strComPort in eggbot_scan.findEiBotBoards(): serialPort = self.testSerialPort( strComPort ) if serialPort != None: return serialPort # Try any likely ports for strComPort in eggbot_scan.findPorts(): serialPort = self.testSerialPort( strComPort ) if serialPort != None: 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() unused_response = self.serialPort.readline() #read in extra blank/OK line except: inkex.errormsg( gettext.gettext( "Error reading serial data." ) ) return response def distance( x, y ): ''' Pythagorean theorem! ''' return sqrt( x * x + y * y ) e = EggBot() #e.affect(output=False) e.affect()