diff options
-rwxr-xr-x | gerber/excellon.py | 68 | ||||
-rw-r--r-- | gerber/excellon_settings.py | 105 | ||||
-rw-r--r-- | gerber/excellon_statements.py | 26 | ||||
-rw-r--r-- | gerber/excellon_tool.py | 111 | ||||
-rw-r--r-- | gerber/primitives.py | 2 |
5 files changed, 300 insertions, 12 deletions
diff --git a/gerber/excellon.py b/gerber/excellon.py index 85821e5..3fb813f 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -32,6 +32,7 @@ except(ImportError): from io import StringIO
from .excellon_statements import *
+from .excellon_tool import ExcellonToolDefinitionParser
from .cam import CamFile, FileSettings
from .primitives import Drill
from .utils import inch, metric
@@ -56,12 +57,15 @@ def read(filename): settings = FileSettings(**detect_excellon_format(data))
return ExcellonParser(settings).parse(filename)
-def loads(data):
+def loads(data, settings = None, tools = None):
""" Read data from string and return an ExcellonFile
Parameters
----------
data : string
string containing Excellon file contents
+
+ tools: dict (optional)
+ externally defined tools
Returns
-------
@@ -70,8 +74,9 @@ def loads(data): """
# File object should use settings from source file by default.
- settings = FileSettings(**detect_excellon_format(data))
- return ExcellonParser(settings).parse_raw(data)
+ if not settings:
+ settings = FileSettings(**detect_excellon_format(data))
+ return ExcellonParser(settings, tools).parse_raw(data)
class DrillHit(object):
@@ -199,7 +204,7 @@ class ExcellonFile(CamFile): for primitive in self.primitives:
primitive.to_inch()
for hit in self.hits:
- hit.position = tuple(map(inch, hit,position))
+ hit.position = tuple(map(inch, hit.position))
def to_metric(self):
@@ -282,7 +287,7 @@ class ExcellonParser(object): settings : FileSettings or dict-like
Excellon file settings to use when interpreting the excellon file.
"""
- def __init__(self, settings=None):
+ def __init__(self, settings=None, ext_tools=None):
self.notation = 'absolute'
self.units = 'inch'
self.zeros = 'leading'
@@ -290,6 +295,8 @@ class ExcellonParser(object): self.state = 'INIT'
self.statements = []
self.tools = {}
+ self.ext_tools = ext_tools or {}
+ self.comment_tools = {}
self.hits = []
self.active_tool = None
self.pos = [0., 0.]
@@ -352,6 +359,18 @@ class ExcellonParser(object): detected_format = tuple([int(x) for x in comment_stmt.comment.split('=')[1].split(":")])
if detected_format:
self.format = detected_format
+
+ if "HEADER:" in comment_stmt.comment:
+ self.state = "HEADER"
+
+ if " Holesize " in comment_stmt.comment:
+ self.state = "HEADER"
+
+ # Parse this as a hole definition
+ tools = ExcellonToolDefinitionParser(self._settings()).parse_raw(comment_stmt.comment)
+ if len(tools) == 1:
+ tool = tools[tools.keys()[0]]
+ self.comment_tools[tool.number] = tool
elif line[:3] == 'M48':
self.statements.append(HeaderBeginStmt())
@@ -363,6 +382,16 @@ class ExcellonParser(object): self.state = 'DRILL'
elif self.state == 'INIT':
self.state = 'HEADER'
+
+ elif line[:3] == 'M00' and self.state == 'DRILL':
+ if self.active_tool:
+ cur_tool_number = self.active_tool.number
+ next_tool = self._get_tool(cur_tool_number + 1)
+
+ self.statements.append(NextToolSelectionStmt(self.active_tool, next_tool))
+ self.active_tool = next_tool
+ else:
+ raise Exception('Invalid state exception')
elif line[:3] == 'M95':
self.statements.append(HeaderEndStmt())
@@ -480,8 +509,10 @@ class ExcellonParser(object): # T0 is used as END marker, just ignore
if stmt.tool != 0:
- # FIXME: for weird files with no tools defined, original calc from gerbv
- if stmt.tool not in self.tools:
+ tool = self._get_tool(stmt.tool)
+
+ if not tool:
+ # FIXME: for weird files with no tools defined, original calc from gerbv
if self._settings().units == "inch":
diameter = (16 + 8 * stmt.tool) / 1000.0;
else:
@@ -496,7 +527,7 @@ class ExcellonParser(object): self.statements.insert(i, tool)
break
- self.active_tool = self.tools[stmt.tool]
+ self.active_tool = tool
elif line[0] == 'R' and self.state != 'HEADER':
stmt = RepeatHoleStmt.from_excellon(line, self._settings())
@@ -523,6 +554,9 @@ class ExcellonParser(object): if y is not None:
self.pos[1] += y
if self.state == 'DRILL':
+ if not self.active_tool:
+ self.active_tool = self._get_tool(1)
+
self.hits.append(DrillHit(self.active_tool, tuple(self.pos)))
self.active_tool._hit()
else:
@@ -531,7 +565,23 @@ class ExcellonParser(object): def _settings(self):
return FileSettings(units=self.units, format=self.format,
zeros=self.zeros, notation=self.notation)
-
+
+ def _get_tool(self, toolid):
+
+ tool = self.tools.get(toolid)
+ if not tool:
+ tool = self.comment_tools.get(toolid)
+ if tool:
+ tool.settings = self._settings()
+ self.tools[toolid] = tool
+
+ if not tool:
+ tool = self.ext_tools.get(toolid)
+ if tool:
+ tool.settings = self._settings()
+ self.tools[toolid] = tool
+
+ return tool
def detect_excellon_format(data=None, filename=None):
""" Detect excellon file decimal format and zero-suppression settings.
diff --git a/gerber/excellon_settings.py b/gerber/excellon_settings.py new file mode 100644 index 0000000..4dbe0ca --- /dev/null +++ b/gerber/excellon_settings.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from argparse import PARSER + +# Copyright 2015 Garret Fick <garret@ficksworkshop.com> + +# 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 + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# 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. + +""" +Excellon Settings Definition File module +==================== +**Excellon file classes** + +This module provides Excellon file classes and parsing utilities +""" + +import re +try: + from cStringIO import StringIO +except(ImportError): + from io import StringIO + +from .cam import FileSettings + +def loads(data): + """ Read settings file information and return an FileSettings + Parameters + ---------- + data : string + string containing Excellon settings file contents + + Returns + ------- + file settings: FileSettings + + """ + + return ExcellonSettingsParser().parse_raw(data) + +def map_coordinates(value): + if value == 'ABSOLUTE': + return 'absolute' + return 'relative' + +def map_units(value): + if value == 'ENGLISH': + return 'inch' + return 'metric' + +def map_boolean(value): + return value == 'YES' + +SETTINGS_KEYS = { + 'INTEGER-PLACES': (int, 'format-int'), + 'DECIMAL-PLACES': (int, 'format-dec'), + 'COORDINATES': (map_coordinates, 'notation'), + 'OUTPUT-UNITS': (map_units, 'units'), + } + +class ExcellonSettingsParser(object): + """Excellon Settings PARSER + + Parameters + ---------- + None + """ + + def __init__(self): + self.values = {} + self.settings = None + + def parse_raw(self, data): + for line in StringIO(data): + self._parse(line.strip()) + + # Create the FileSettings object + self.settings = FileSettings( + notation=self.values['notation'], + units=self.values['units'], + format=(self.values['format-int'], self.values['format-dec']) + ) + + return self.settings + + def _parse(self, line): + + line_items = line.split() + if len(line_items) == 2: + + item_type_info = SETTINGS_KEYS.get(line_items[0]) + if item_type_info: + # Convert the value to the expected type + item_value = item_type_info[0](line_items[1]) + + self.values[item_type_info[1]] = item_value
\ No newline at end of file diff --git a/gerber/excellon_statements.py b/gerber/excellon_statements.py index 2be7a05..9499c51 100644 --- a/gerber/excellon_statements.py +++ b/gerber/excellon_statements.py @@ -36,7 +36,8 @@ __all__ = ['ExcellonTool', 'ToolSelectionStmt', 'CoordinateStmt', 'ExcellonStatement', 'ZAxisRoutPositionStmt', 'RetractWithClampingStmt', 'RetractWithoutClampingStmt', 'CutterCompensationOffStmt', 'CutterCompensationLeftStmt', - 'CutterCompensationRightStmt', 'ZAxisInfeedRateStmt'] + 'CutterCompensationRightStmt', 'ZAxisInfeedRateStmt', + 'NextToolSelectionStmt'] class ExcellonStatement(object): @@ -267,7 +268,28 @@ class ToolSelectionStmt(ExcellonStatement): if self.compensation_index is not None: stmt += '%02d' % self.compensation_index return stmt - + +class NextToolSelectionStmt(ExcellonStatement): + + # TODO the statement exists outside of the context of the file, + # so it is imposible to know that it is really the next tool + + def __init__(self, cur_tool, next_tool, **kwargs): + """ + Select the next tool in the wheel. + Parameters + ---------- + cur_tool : the tool that is currently selected + next_tool : the that that is now selected + """ + super(NextToolSelectionStmt, self).__init__(**kwargs) + + self.cur_tool = cur_tool + self.next_tool = next_tool + + def to_excellon(self, settings=None): + stmt = 'M00' + return stmt class ZAxisInfeedRateStmt(ExcellonStatement): diff --git a/gerber/excellon_tool.py b/gerber/excellon_tool.py new file mode 100644 index 0000000..b7d67d4 --- /dev/null +++ b/gerber/excellon_tool.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2015 Garret Fick <garret@ficksworkshop.com> + +# 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 + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# 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. + +""" +Excellon Tool Definition File module +==================== +**Excellon file classes** + +This module provides Excellon file classes and parsing utilities +""" + +import re +try: + from cStringIO import StringIO +except(ImportError): + from io import StringIO + +from .excellon_statements import ExcellonTool + +def loads(data, settings=None): + """ Read tool file information and return a map of tools + Parameters + ---------- + data : string + string containing Excellon Tool Definition file contents + + Returns + ------- + dict tool name: ExcellonTool + + """ + return ExcellonToolDefinitionParser(settings).parse_raw(data) + +class ExcellonToolDefinitionParser(object): + """ Excellon File Parser + + Parameters + ---------- + None + """ + + allegro_tool = re.compile(r'(?P<size>[0-9/.]+)\s+(?P<plated>P|N)\s+T(?P<toolid>[0-9]{2})\s+(?P<xtol>[0-9/.]+)\s+(?P<ytol>[0-9/.]+)') + allegro_comment_mils = re.compile('Holesize (?P<toolid>[0-9]{1,2})\. = (?P<size>[0-9/.]+) Tolerance = \+(?P<xtol>[0-9/.]+)/-(?P<ytol>[0-9/.]+) (?P<plated>(PLATED)|(NON_PLATED)) MILS Quantity = [0-9]+') + allegro_comment_mm = re.compile('Holesize (?P<toolid>[0-9]{1,2})\. = (?P<size>[0-9/.]+) Tolerance = \+(?P<xtol>[0-9/.]+)/-(?P<ytol>[0-9/.]+) (?P<plated>(PLATED)|(NON_PLATED)) MM Quantity = [0-9]+') + + matchers = [ + (allegro_tool, 'mils'), + (allegro_comment_mils, 'mils'), + (allegro_comment_mils, 'mm'), + ] + + def __init__(self, settings=None): + self.tools = {} + self.settings = settings + + def parse_raw(self, data): + for line in StringIO(data): + self._parse(line.strip()) + + return self.tools + + def _parse(self, line): + + for matcher in ExcellonToolDefinitionParser.matchers: + m = matcher[0].match(line) + if m: + unit = matcher[1] + + size = float(m.group('size')) + plated = m.group('plated') + toolid = int(m.group('toolid')) + xtol = float(m.group('xtol')) + ytol = float(m.group('ytol')) + + size = self._convert_length(size, unit) + xtol = self._convert_length(xtol, unit) + ytol = self._convert_length(ytol, unit) + + tool = ExcellonTool(None, number=toolid, diameter=size) + + self.tools[tool.number] = tool + + break + + def _convert_length(self, value, unit): + + # Convert the value to mm + if unit == 'mils': + value /= 39.3700787402 + + # Now convert to the settings unit + if self.settings.units == 'inch': + return value / 25.4 + else: + # Already in mm + return value +
\ No newline at end of file diff --git a/gerber/primitives.py b/gerber/primitives.py index 3f68496..3c630f0 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -755,7 +755,7 @@ class Drill(Primitive): validate_coordinates(position)
self.position = position
self.diameter = diameter
- self.hit = hit
+ self.hit = hit
self._to_convert = ['position', 'diameter']
@property
|