''' 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()