From 9ca75f991a240b0ea233382ff23264a009b0324e Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Fri, 13 Nov 2015 03:31:32 -0200 Subject: Improve Excellon parsing coverage Add some not so used codes that were generating unknown stmt. --- gerber/excellon.py | 83 +++++++++++++++++++---- gerber/excellon_statements.py | 109 +++++++++++++++++++++++++++++-- gerber/tests/test_excellon_statements.py | 50 ++++++++++++++ 3 files changed, 226 insertions(+), 16 deletions(-) (limited to 'gerber') 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') -- cgit