Improve Excellon parsing coverage
Add some not so used codes that were generating unknown stmt.
This commit is contained in:
parent
944c832922
commit
9ca75f991a
3 changed files with 226 additions and 16 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue