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:
parent
ac89a3c365
commit
137c73f3e4
3 changed files with 144 additions and 56 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue