From 0bd00464de4df8869fdfaf63dff7a17b12e0f1aa Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Sun, 5 Jan 2014 16:27:57 -0500 Subject: [PATCH] Minimal Excellon class and test script --- camlib.py | 123 +++++++++++++++++++++++++++++++++++++++++++-- camlib.pyc | Bin 48806 -> 53244 bytes test_excellon_1.py | 45 +++++++++++++++++ 3 files changed, 165 insertions(+), 3 deletions(-) create mode 100644 test_excellon_1.py diff --git a/camlib.py b/camlib.py index d7a79df0..c18b4913 100644 --- a/camlib.py +++ b/camlib.py @@ -228,7 +228,7 @@ class Gerber (Geometry): [poly['polygon'] for poly in self.regions] + flash_polys) -class CNCjob(): +class CNCjob: def __init__(self, units="in", kind="generic", z_move = 0.1, feedrate = 3.0, z_cut = -0.002): # Options @@ -256,6 +256,45 @@ class CNCjob(): # Output generated by CNCjob.create_gcode_geometry() self.G_geometry = None + def generate_from_excellon(self, exobj): + ''' + Generates G-code for drilling from excellon text. + self.gcode becomes a list, each element is a + different job for each tool in the excellon code. + ''' + self.kind = "drill" + self.gcode = [] + + t = "G00 X%.4fY%.4f\n" + down = "G01 Z%.4f\n"%self.z_cut + up = "G01 Z%.4f\n"%self.z_move + + for tool in exobj.tools: + + points = [] + gcode = "" + + for drill in exobj.drill: + if drill['tool'] == tool: + points.append(drill['point']) + + gcode = self.unitcode[self.units] + "\n" + gcode += self.absolutecode + "\n" + gcode += self.feedminutecode + "\n" + gcode += "F%.2f\n"%self.feedrate + gcode += "G00 Z%.4f\n"%self.z_move # Move to travel height + gcode += "M03\n" # Spindle start + gcode += self.pausecode + "\n" + + for point in points: + gcode += t%point + gcode += down + up + + gcode += t%(0,0) + gcode += "M05\n" # Spindle stop + + self.gcode.append(gcode) + def generate_from_geometry(self, geometry, append=True, tooldia=None): ''' Generates G-Code for geometry (Shapely collection). @@ -314,15 +353,19 @@ class CNCjob(): print "WARNING: G-code generation not implemented for %s"%(str(type(geo))) + self.gcode += "G00 Z%.4f\n"%self.z_move # Stop cutting + self.gcode += "G00 X0Y0\n" self.gcode += "M05\n" # Spindle stop def create_gcode_geometry(self): ''' - G-Code parser. Generates dictionary with single-segment LineString's - and "kind" indicating cut or travel, fast or feedrate speed. + G-Code parser (from self.gcode). Generates dictionary with + single-segment LineString's and "kind" indicating cut or travel, + fast or feedrate speed. ''' geometry = [] + # TODO: ???? bring this into the class?? gobjs = gparse1b(self.gcode) # Last known instruction @@ -400,6 +443,80 @@ class CNCjob(): return fig + +class Excellon(Geometry): + def __init__(self): + Geometry.__init__(self) + + self.tools = {} + + self.drills = [] + + def parse_file(self, filename): + efile = open(filename, 'r') + estr = efile.readlines() + efile.close() + self.parse_lines(estr) + + def parse_lines(self, elines): + ''' + Main Excellon parser. + ''' + current_tool = "" + + for eline in elines: + + ## Tool definitions ## + # TODO: Verify all this + indexT = eline.find("T") + indexC = eline.find("C") + indexF = eline.find("F") + # Type 1 + if indexT != -1 and indexC > indexT and indexF > indexF: + tool = eline[1:indexC] + spec = eline[indexC+1:indexF] + self.tools[tool] = spec + continue + # Type 2 + # TODO: Is this inches? + #indexsp = eline.find(" ") + #indexin = eline.find("in") + #if indexT != -1 and indexsp > indexT and indexin > indexsp: + # tool = eline[1:indexsp] + # spec = eline[indexsp+1:indexin] + # self.tools[tool] = spec + # continue + # Type 3 + if indexT != -1 and indexC > indexT: + tool = eline[1:indexC] + spec = eline[indexC+1:-1] + self.tools[tool] = spec + continue + + ## Tool change + if indexT == 0: + current_tool = eline[1:-1] + continue + + ## Drill + indexX = eline.find("X") + indexY = eline.find("Y") + if indexX != -1 and indexY != -1: + x = float(int(eline[indexX+1:indexY])/10000.0) + y = float(int(eline[indexY+1:-1])/10000.0) + self.drills.append({'point':Point((x,y)), 'tool':current_tool}) + continue + + print "WARNING: Line ignored:", eline + + def create_geometry(self): + self.solid_geometry = [] + sizes = {} + for tool in self.tools: + sizes[tool] = float(self.tools[tool]) + for drill in self.drills: + poly = Point(drill['point']).buffer(sizes[drill['tool']]/2.0) + self.solid_geometry.append(poly) def fix_poly(poly): ''' diff --git a/camlib.pyc b/camlib.pyc index 05a5333a923e174edd30c4891557761e51ca1e8e..3970c11bf89155d279916ffe4f78cc52228d92ad 100644 GIT binary patch delta 11802 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 delta 7577 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 Mg|dWt