summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xgerber/excellon.py83
-rw-r--r--gerber/excellon_statements.py109
-rw-r--r--gerber/tests/test_excellon_statements.py50
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')