diff options
-rwxr-xr-x | gerber/excellon.py | 83 | ||||
-rw-r--r-- | gerber/excellon_statements.py | 109 | ||||
-rw-r--r-- | gerber/tests/test_excellon_statements.py | 50 |
3 files changed, 226 insertions, 16 deletions
diff --git a/gerber/excellon.py b/gerber/excellon.py index 7333a98..c953e55 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -78,12 +78,12 @@ class DrillHit(object): def __init__(self, tool, position):
self.tool = tool
self.position = position
-
+
def to_inch(self):
if self.tool.units == 'metric':
self.tool.to_inch()
self.position = tuple(map(inch, self.position))
-
+
def to_metric(self):
if self.tool.units == 'inch':
self.tool.to_metric()
@@ -96,6 +96,7 @@ class ExcellonFile(CamFile): The ExcellonFile class represents a single excellon file.
http://www.excellon.com/manuals/program.htm
+ (archived version at https://web.archive.org/web/20150920001043/http://www.excellon.com/manuals/program.htm)
Parameters
----------
@@ -122,11 +123,11 @@ class ExcellonFile(CamFile): filename=filename)
self.tools = tools
self.hits = hits
-
+
@property
def primitives(self):
return [Drill(hit.position, hit.tool.diameter,units=self.settings.units) for hit in self.hits]
-
+
@property
def bounds(self):
@@ -169,14 +170,14 @@ class ExcellonFile(CamFile): def write(self, filename=None):
filename = filename if filename is not None else self.filename
with open(filename, 'w') as f:
-
+
# Copy the header verbatim
for statement in self.statements:
if not isinstance(statement, ToolSelectionStmt):
f.write(statement.to_excellon(self.settings) + '\n')
else:
break
-
+
# Write out coordinates for drill hits by tool
for tool in iter(self.tools.values()):
f.write(ToolSelectionStmt(tool.number).to_excellon(self.settings) + '\n')
@@ -184,7 +185,7 @@ class ExcellonFile(CamFile): if hit.tool.number == tool.number:
f.write(CoordinateStmt(*hit.position).to_excellon(self.settings) + '\n')
f.write(EndOfProgramStmt().to_excellon() + '\n')
-
+
def to_inch(self):
"""
Convert units to inches
@@ -235,7 +236,7 @@ class ExcellonFile(CamFile): lengths[num] = 0.0 if lengths.get(num) is None else lengths[num]
lengths[num] = lengths[num] + math.hypot(*tuple(map(operator.sub, positions[num], hit.position)))
positions[num] = hit.position
-
+
if tool_number is None:
return lengths
else:
@@ -270,8 +271,8 @@ class ExcellonFile(CamFile): for hit in self.hits:
if hit.tool.number == newtool.number:
hit.tool = newtool
-
-
+
+
class ExcellonParser(object):
""" Excellon File Parser
@@ -368,6 +369,15 @@ class ExcellonParser(object): if self.state == 'HEADER':
self.state = 'DRILL'
+ elif line[:3] == 'M15':
+ self.statements.append(ZAxisRoutPositionStmt())
+
+ elif line[:3] == 'M16':
+ self.statements.append(RetractWithClampingStmt())
+
+ elif line[:3] == 'M17':
+ self.statements.append(RetractWithoutClampingStmt())
+
elif line[:3] == 'M30':
stmt = EndOfProgramStmt.from_excellon(line, self._settings())
self.statements.append(stmt)
@@ -376,6 +386,44 @@ class ExcellonParser(object): self.statements.append(RouteModeStmt())
self.state = 'ROUT'
+ stmt = CoordinateStmt.from_excellon(line[3:], self._settings())
+ stmt.mode = self.state
+
+ x = stmt.x
+ y = stmt.y
+ self.statements.append(stmt)
+ if self.notation == 'absolute':
+ if x is not None:
+ self.pos[0] = x
+ if y is not None:
+ self.pos[1] = y
+ else:
+ if x is not None:
+ self.pos[0] += x
+ if y is not None:
+ self.pos[1] += y
+
+ elif line[:3] == 'G01':
+ self.statements.append(RouteModeStmt())
+ self.state = 'LINEAR'
+
+ stmt = CoordinateStmt.from_excellon(line[3:], self._settings())
+ stmt.mode = self.state
+
+ x = stmt.x
+ y = stmt.y
+ self.statements.append(stmt)
+ if self.notation == 'absolute':
+ if x is not None:
+ self.pos[0] = x
+ if y is not None:
+ self.pos[1] = y
+ else:
+ if x is not None:
+ self.pos[0] += x
+ if y is not None:
+ self.pos[1] += y
+
elif line[:3] == 'G05':
self.statements.append(DrillModeStmt())
self.state = 'DRILL'
@@ -404,10 +452,23 @@ class ExcellonParser(object): stmt = FormatStmt.from_excellon(line)
self.statements.append(stmt)
+ elif line[:3] == 'G40':
+ self.statements.append(CutterCompensationOffStmt())
+
+ elif line[:3] == 'G41':
+ self.statements.append(CutterCompensationLeftStmt())
+
+ elif line[:3] == 'G42':
+ self.statements.append(CutterCompensationRightStmt())
+
elif line[:3] == 'G90':
self.statements.append(AbsoluteModeStmt())
self.notation = 'absolute'
+ elif line[0] == 'F':
+ infeed_rate_stmt = ZAxisInfeedRateStmt.from_excellon(line)
+ self.statements.append(infeed_rate_stmt)
+
elif line[0] == 'T' and self.state == 'HEADER':
tool = ExcellonTool.from_excellon(line, self._settings())
self.tools[tool.number] = tool
@@ -475,7 +536,7 @@ def detect_excellon_format(data=None, filename=None): detected_format = None
zeros_options = ('leading', 'trailing', )
format_options = ((2, 4), (2, 5), (3, 3),)
-
+
if data is None and filename is None:
raise ValueError('Either data or filename arguments must be provided')
if data is None:
diff --git a/gerber/excellon_statements.py b/gerber/excellon_statements.py index fa05e53..2be7a05 100644 --- a/gerber/excellon_statements.py +++ b/gerber/excellon_statements.py @@ -31,24 +31,27 @@ __all__ = ['ExcellonTool', 'ToolSelectionStmt', 'CoordinateStmt', 'CommentStmt', 'HeaderBeginStmt', 'HeaderEndStmt', 'RewindStopStmt', 'EndOfProgramStmt', 'UnitStmt', 'IncrementalModeStmt', 'VersionStmt', 'FormatStmt', 'LinkToolStmt', - 'MeasuringModeStmt', 'RouteModeStmt', 'DrillModeStmt', + 'MeasuringModeStmt', 'RouteModeStmt', 'LinearModeStmt', 'DrillModeStmt', 'AbsoluteModeStmt', 'RepeatHoleStmt', 'UnknownStmt', - 'ExcellonStatement',] + 'ExcellonStatement', 'ZAxisRoutPositionStmt', + 'RetractWithClampingStmt', 'RetractWithoutClampingStmt', + 'CutterCompensationOffStmt', 'CutterCompensationLeftStmt', + 'CutterCompensationRightStmt', 'ZAxisInfeedRateStmt'] class ExcellonStatement(object): """ Excellon Statement abstract base class """ - + @classmethod def from_excellon(cls, line): raise NotImplementedError('from_excellon must be implemented in a ' 'subclass') - + def __init__(self, unit='inch', id=None): self.units = unit self.id = uuid.uuid4().int if id is None else id - + def to_excellon(self, settings=None): raise NotImplementedError('to_excellon must be implemented in a ' 'subclass') @@ -266,6 +269,34 @@ class ToolSelectionStmt(ExcellonStatement): return stmt +class ZAxisInfeedRateStmt(ExcellonStatement): + + @classmethod + def from_excellon(cls, line, **kwargs): + """ Create a ZAxisInfeedRate from an excellon file line. + + Parameters + ---------- + line : string + Line from an Excellon file + + Returns + ------- + z_axis_infeed_rate : ToolSelectionStmt + ToolSelectionStmt representation of `line.` + """ + rate = int(line[1:]) + + return cls(rate, **kwargs) + + def __init__(self, rate, **kwargs): + super(ZAxisInfeedRateStmt, self).__init__(**kwargs) + self.rate = rate + + def to_excellon(self, settings=None): + return 'F%02d' % self.rate + + class CoordinateStmt(ExcellonStatement): @classmethod @@ -290,9 +321,14 @@ class CoordinateStmt(ExcellonStatement): super(CoordinateStmt, self).__init__(**kwargs) self.x = x self.y = y + self.mode = None def to_excellon(self, settings): stmt = '' + if self.mode == "ROUT": + stmt += "G00" + if self.mode == "LINEAR": + stmt += "G01" if self.x is not None: stmt += 'X%s' % write_gerber_value(self.x, settings.format, settings.zero_suppression) @@ -431,6 +467,60 @@ class RewindStopStmt(ExcellonStatement): return '%' +class ZAxisRoutPositionStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(ZAxisRoutPositionStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'M15' + + +class RetractWithClampingStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(RetractWithClampingStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'M16' + + +class RetractWithoutClampingStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(RetractWithoutClampingStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'M17' + + +class CutterCompensationOffStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(CutterCompensationOffStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'G40' + + +class CutterCompensationLeftStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(CutterCompensationLeftStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'G41' + + +class CutterCompensationRightStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(CutterCompensationRightStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'G42' + + class EndOfProgramStmt(ExcellonStatement): @classmethod @@ -608,6 +698,15 @@ class RouteModeStmt(ExcellonStatement): return 'G00' +class LinearModeStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(LinearModeStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'G01' + + class DrillModeStmt(ExcellonStatement): def __init__(self, **kwargs): diff --git a/gerber/tests/test_excellon_statements.py b/gerber/tests/test_excellon_statements.py index 1e8ef91..2f0ef10 100644 --- a/gerber/tests/test_excellon_statements.py +++ b/gerber/tests/test_excellon_statements.py @@ -123,6 +123,28 @@ def test_toolselection_dump(): stmt = ToolSelectionStmt.from_excellon(line) assert_equal(stmt.to_excellon(), line) +def test_z_axis_infeed_rate_factory(): + """ Test ZAxisInfeedRateStmt factory method + """ + stmt = ZAxisInfeedRateStmt.from_excellon('F01') + assert_equal(stmt.rate, 1) + stmt = ZAxisInfeedRateStmt.from_excellon('F2') + assert_equal(stmt.rate, 2) + stmt = ZAxisInfeedRateStmt.from_excellon('F03') + assert_equal(stmt.rate, 3) + +def test_z_axis_infeed_rate_dump(): + """ Test ZAxisInfeedRateStmt to_excellon() + """ + inputs = [ + ('F01', 'F01'), + ('F2', 'F02'), + ('F00003', 'F03') + ] + for input_rate, expected_output in inputs: + stmt = ZAxisInfeedRateStmt.from_excellon(input_rate) + assert_equal(stmt.to_excellon(), expected_output) + def test_coordinatestmt_factory(): """ Test CoordinateStmt factory method """ @@ -323,6 +345,30 @@ def test_rewindstop_stmt(): stmt = RewindStopStmt() assert_equal(stmt.to_excellon(None), '%') +def test_z_axis_rout_position_stmt(): + stmt = ZAxisRoutPositionStmt() + assert_equal(stmt.to_excellon(None), 'M15') + +def test_retract_with_clamping_stmt(): + stmt = RetractWithClampingStmt() + assert_equal(stmt.to_excellon(None), 'M16') + +def test_retract_without_clamping_stmt(): + stmt = RetractWithoutClampingStmt() + assert_equal(stmt.to_excellon(None), 'M17') + +def test_cutter_compensation_off_stmt(): + stmt = CutterCompensationOffStmt() + assert_equal(stmt.to_excellon(None), 'G40') + +def test_cutter_compensation_left_stmt(): + stmt = CutterCompensationLeftStmt() + assert_equal(stmt.to_excellon(None), 'G41') + +def test_cutter_compensation_right_stmt(): + stmt = CutterCompensationRightStmt() + assert_equal(stmt.to_excellon(None), 'G42') + def test_endofprogramstmt_factory(): settings = FileSettings(units='inch') stmt = EndOfProgramStmt.from_excellon('M30X01Y02', settings) @@ -579,6 +625,10 @@ def test_routemode_stmt(): stmt = RouteModeStmt() assert_equal(stmt.to_excellon(FileSettings()), 'G00') +def test_linearmode_stmt(): + stmt = LinearModeStmt() + assert_equal(stmt.to_excellon(FileSettings()), 'G01') + def test_drillmode_stmt(): stmt = DrillModeStmt() assert_equal(stmt.to_excellon(FileSettings()), 'G05') |