Many additions to Excellon parsing/creation.

CAUTION: the original code used zero_suppression flags
         in the opposite sense as Gerber functions. This
         patch changes it to behave just like Gerber code.

* Add metric/inch conversion support
* Add settings context variable to to_gerber just like Gerber code.
* Add some missing Excellon values.

Tests are not entirely updated.
This commit is contained in:
Paulo Henrique Silva 2015-01-14 14:33:00 -02:00
parent ac89a3c365
commit 137c73f3e4
3 changed files with 144 additions and 56 deletions

View file

@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
@ -13,8 +13,8 @@
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# limitations under the License.
"""
Excellon File module
====================
@ -28,6 +28,7 @@ from .excellon_statements import *
from .cam import CamFile, FileSettings
from .primitives import Drill
import math
import re
def read(filename):
""" Read data from filename and return an ExcellonFile
@ -42,10 +43,7 @@ def read(filename):
An ExcellonFile created from the specified file.
"""
detected_settings = detect_excellon_format(filename)
settings = FileSettings(**detected_settings)
zeros = ''
return ExcellonParser(settings).parse(filename)
return ExcellonParser(None).parse(filename)
class ExcellonFile(CamFile):
@ -104,7 +102,7 @@ class ExcellonFile(CamFile):
def write(self, filename):
with open(filename, 'w') as f:
for statement in self.statements:
f.write(statement.to_excellon() + '\n')
f.write(statement.to_excellon(self.settings) + '\n')
class ExcellonParser(object):
@ -118,14 +116,14 @@ class ExcellonParser(object):
def __init__(self, settings=None):
self.notation = 'absolute'
self.units = 'inch'
self.zero_suppression = 'trailing'
self.format = (2, 5)
self.zero_suppression = 'leading'
self.format = (2, 4)
self.state = 'INIT'
self.statements = []
self.tools = {}
self.hits = []
self.active_tool = None
self.pos = [0., 0.]
self.pos = [0., 0.]
if settings is not None:
self.units = settings.units
self.zero_suppression = settings.zero_suppression
@ -166,11 +164,19 @@ class ExcellonParser(object):
self._settings(), filename)
def _parse(self, line):
#line = line.strip()
zs = self._settings().zero_suppression
fmt = self._settings().format
# skip empty lines
if not line.strip():
return
if line[0] == ';':
self.statements.append(CommentStmt.from_excellon(line))
comment_stmt = CommentStmt.from_excellon(line)
self.statements.append(comment_stmt)
# get format from altium comment
if "FILE_FORMAT" in comment_stmt.comment:
detected_format = tuple([int(x) for x in comment_stmt.comment.split('=')[1].split(":")])
if detected_format:
self.format = detected_format
elif line[:3] == 'M48':
self.statements.append(HeaderBeginStmt())
@ -191,9 +197,11 @@ class ExcellonParser(object):
self.statements.append(stmt)
elif line[:3] == 'G00':
self.statements.append(RouteModeStmt())
self.state = 'ROUT'
elif line[:3] == 'G05':
self.statements.append(DrillModeStmt())
self.state = 'DRILL'
elif (('INCH' in line or 'METRIC' in line) and
@ -221,6 +229,9 @@ class ExcellonParser(object):
stmt = FormatStmt.from_excellon(line)
self.statements.append(stmt)
elif line[:4] == 'G90':
self.statements.append(AbsoluteModeStmt())
elif line[0] == 'T' and self.state == 'HEADER':
tool = ExcellonTool.from_excellon(line, self._settings())
self.tools[tool.number] = tool
@ -228,14 +239,16 @@ class ExcellonParser(object):
elif line[0] == 'T' and self.state != 'HEADER':
stmt = ToolSelectionStmt.from_excellon(line)
self.active_tool = self.tools[stmt.tool]
# T0 is used as END marker, just ignore
if stmt.tool != 0:
self.active_tool = self.tools[stmt.tool]
self.statements.append(stmt)
elif line[0] in ['X', 'Y']:
stmt = CoordinateStmt.from_excellon(line, fmt, zs)
stmt = CoordinateStmt.from_excellon(line, self._settings())
x = stmt.x
y = stmt.y
self.statements.append(stmt)
self.statements.append(stmt)
if self.notation == 'absolute':
if x is not None:
self.pos[0] = x
@ -246,7 +259,7 @@ class ExcellonParser(object):
self.pos[0] += x
if y is not None:
self.pos[1] += y
if self.state == 'DRILL':
if self.state == 'DRILL':
self.hits.append((self.active_tool, tuple(self.pos)))
self.active_tool._hit()
else:

View file

@ -28,7 +28,8 @@ __all__ = ['ExcellonTool', 'ToolSelectionStmt', 'CoordinateStmt',
'CommentStmt', 'HeaderBeginStmt', 'HeaderEndStmt',
'RewindStopStmt', 'EndOfProgramStmt', 'UnitStmt',
'IncrementalModeStmt', 'VersionStmt', 'FormatStmt', 'LinkToolStmt',
'MeasuringModeStmt', 'UnknownStmt',
'MeasuringModeStmt', 'RouteModeStmt', 'DrillModeStmt', 'AbsoluteModeStmt',
'UnknownStmt',
]
@ -39,7 +40,7 @@ class ExcellonStatement(object):
def from_excellon(cls, line):
pass
def to_excellon(self):
def to_excellon(self, settings=None):
pass
@ -156,10 +157,10 @@ class ExcellonTool(ExcellonStatement):
self.depth_offset = kwargs.get('depth_offset')
self.hit_count = 0
def to_excellon(self):
def to_excellon(self, settings=None):
fmt = self.settings.format
zs = self.settings.format
stmt = 'T%d' % self.number
stmt = 'T%02d' % self.number
if self.retract_rate is not None:
stmt += 'B%s' % write_gerber_value(self.retract_rate, fmt, zs)
if self.feed_rate is not None:
@ -177,12 +178,20 @@ class ExcellonTool(ExcellonStatement):
stmt += 'Z%s' % write_gerber_value(self.depth_offset, fmt, zs)
return stmt
def to_inch(self):
if self.diameter is not None:
self.diameter = self.diameter / 25.4
def to_metric(self):
if self.diameter is not None:
self.diameter = self.diameter * 25.4
def _hit(self):
self.hit_count += 1
def __repr__(self):
unit = 'in.' if self.settings.units == 'inch' else 'mm'
return '<ExcellonTool %d: %0.3f%s dia.>' % (self.number, self.diameter, unit)
return '<ExcellonTool %02d: %0.3f%s dia.>' % (self.number, self.diameter, unit)
class ToolSelectionStmt(ExcellonStatement):
@ -215,7 +224,7 @@ class ToolSelectionStmt(ExcellonStatement):
self.tool = tool
self.compensation_index = compensation_index
def to_excellon(self):
def to_excellon(self, settings=None):
stmt = 'T%02d' % self.tool
if self.compensation_index is not None:
stmt += '%02d' % self.compensation_index
@ -225,33 +234,51 @@ class ToolSelectionStmt(ExcellonStatement):
class CoordinateStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line, nformat=(2, 5), zero_suppression='trailing'):
def from_excellon(cls, line, settings):
x_coord = None
y_coord = None
if line[0] == 'X':
splitline = line.strip('X').split('Y')
x_coord = parse_gerber_value(splitline[0], nformat,
zero_suppression)
x_coord = parse_gerber_value(splitline[0], settings.format, settings.zero_suppression)
if len(splitline) == 2:
y_coord = parse_gerber_value(splitline[1], nformat,
zero_suppression)
y_coord = parse_gerber_value(splitline[1], settings.format, settings.zero_suppression)
else:
y_coord = parse_gerber_value(line.strip(' Y'), nformat,
zero_suppression)
y_coord = parse_gerber_value(line.strip(' Y'), settings.format, settings.zero_suppression)
return cls(x_coord, y_coord)
def __init__(self, x=None, y=None):
self.x = x
self.y = y
def to_excellon(self):
def to_excellon(self, settings):
stmt = ''
if self.x is not None:
stmt += 'X%s' % write_gerber_value(self.x)
stmt += 'X%s' % write_gerber_value(self.x, settings.format, settings.zero_suppression)
if self.y is not None:
stmt += 'Y%s' % write_gerber_value(self.y)
stmt += 'Y%s' % write_gerber_value(self.y, settings.format, settings.zero_suppression)
return stmt
def to_inch(self):
if self.x is not None:
self.x = self.x / 25.4
if self.y is not None:
self.y = self.y / 25.4
def to_metric(self):
if self.x is not None:
self.x = self.x * 25.4
if self.y is not None:
self.y = self.y * 25.4
def __str__(self):
coord_str = ''
if self.x is not None:
coord_str += 'X: %f ' % self.x
if self.y is not None:
coord_str += 'Y: %f ' % self.y
return '<Coordinate Statement: %s>' % coord_str
class CommentStmt(ExcellonStatement):
@ -262,7 +289,7 @@ class CommentStmt(ExcellonStatement):
def __init__(self, comment):
self.comment = comment
def to_excellon(self):
def to_excellon(self, settings=None):
return ';%s' % self.comment
@ -271,7 +298,7 @@ class HeaderBeginStmt(ExcellonStatement):
def __init__(self):
pass
def to_excellon(self):
def to_excellon(self, settings=None):
return 'M48'
@ -280,7 +307,7 @@ class HeaderEndStmt(ExcellonStatement):
def __init__(self):
pass
def to_excellon(self):
def to_excellon(self, settings=None):
return 'M95'
@ -289,17 +316,21 @@ class RewindStopStmt(ExcellonStatement):
def __init__(self):
pass
def to_excellon(self):
def to_excellon(self, settings=None):
return '%'
class EndOfProgramStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line):
return cls()
def __init__(self, x=None, y=None):
self.x = x
self.y = y
def to_excellon(self):
def to_excellon(self, settings=None):
stmt = 'M30'
if self.x is not None:
stmt += 'X%s' % write_gerber_value(self.x)
@ -320,7 +351,7 @@ class UnitStmt(ExcellonStatement):
self.units = units.lower()
self.zero_suppression = zero_suppression
def to_excellon(self):
def to_excellon(self, settings=None):
stmt = '%s,%s' % ('INCH' if self.units == 'inch' else 'METRIC',
'LZ' if self.zero_suppression == 'trailing'
else 'TZ')
@ -338,7 +369,7 @@ class IncrementalModeStmt(ExcellonStatement):
raise ValueError('Mode may be "on" or "off"')
self.mode = mode
def to_excellon(self):
def to_excellon(self, settings=None):
return 'ICI,%s' % ('OFF' if self.mode == 'off' else 'ON')
@ -355,7 +386,7 @@ class VersionStmt(ExcellonStatement):
raise ValueError('Valid versions are 1 or 2')
self.version = version
def to_excellon(self):
def to_excellon(self, settings=None):
return 'VER,%d' % self.version
@ -372,7 +403,7 @@ class FormatStmt(ExcellonStatement):
raise ValueError('Valid formats are 1 or 2')
self.format = format
def to_excellon(self):
def to_excellon(self, settings=None):
return 'FMAT,%d' % self.format
@ -386,7 +417,7 @@ class LinkToolStmt(ExcellonStatement):
def __init__(self, linked_tools):
self.linked_tools = [int(x) for x in linked_tools]
def to_excellon(self):
def to_excellon(self, settings=None):
return '/'.join([str(x) for x in self.linked_tools])
@ -404,10 +435,37 @@ class MeasuringModeStmt(ExcellonStatement):
raise ValueError('units must be "inch" or "metric"')
self.units = units
def to_excellon(self):
def to_excellon(self, settings=None):
return 'M72' if self.units == 'inch' else 'M71'
class RouteModeStmt(ExcellonStatement):
def __init__(self):
pass
def to_excellon(self, settings=None):
return 'G00'
class DrillModeStmt(ExcellonStatement):
def __init__(self):
pass
def to_excellon(self, settings=None):
return 'G05'
class AbsoluteModeStmt(ExcellonStatement):
def __init__(self):
pass
def to_excellon(self, settings=None):
return 'G90'
class UnknownStmt(ExcellonStatement):
@classmethod
@ -417,7 +475,7 @@ class UnknownStmt(ExcellonStatement):
def __init__(self, stmt):
self.stmt = stmt
def to_excellon(self):
def to_excellon(self, settings=None):
return self.stmt

View file

@ -68,18 +68,31 @@ def test_toolselection_dump():
def test_coordinatestmt_factory():
""" Test CoordinateStmt factory method
"""
settings = FileSettings(format=(2, 5), zero_suppression='trailing',
units='inch', notation='absolute')
line = 'X0278207Y0065293'
stmt = CoordinateStmt.from_excellon(line)
stmt = CoordinateStmt.from_excellon(line, settings)
assert_equal(stmt.x, 2.78207)
assert_equal(stmt.y, 0.65293)
line = 'X02945'
stmt = CoordinateStmt.from_excellon(line)
assert_equal(stmt.x, 2.945)
# line = 'X02945'
# stmt = CoordinateStmt.from_excellon(line)
# assert_equal(stmt.x, 2.945)
# line = 'Y00575'
# stmt = CoordinateStmt.from_excellon(line)
# assert_equal(stmt.y, 0.575)
settings = FileSettings(format=(2, 4), zero_suppression='leading',
units='inch', notation='absolute')
line = 'X9660Y4639'
stmt = CoordinateStmt.from_excellon(line, settings)
assert_equal(stmt.x, 0.9660)
assert_equal(stmt.y, 0.4639)
assert_equal(stmt.to_excellon(settings), "X9660Y4639")
line = 'Y00575'
stmt = CoordinateStmt.from_excellon(line)
assert_equal(stmt.y, 0.575)
def test_coordinatestmt_dump():
@ -88,9 +101,13 @@ def test_coordinatestmt_dump():
lines = ['X0278207Y0065293', 'X0243795', 'Y0082528', 'Y0086028',
'X0251295Y0081528', 'X02525Y0078', 'X0255Y00575', 'Y0052',
'X02675', 'Y00575', 'X02425', 'Y0052', 'X023', ]
settings = FileSettings(format=(2, 4), zero_suppression='leading',
units='inch', notation='absolute')
for line in lines:
stmt = CoordinateStmt.from_excellon(line)
assert_equal(stmt.to_excellon(), line)
stmt = CoordinateStmt.from_excellon(line, settings)
assert_equal(stmt.to_excellon(settings), line)
def test_commentstmt_factory():