From 4e838df32ac6d283429e30d2a3151b7d7e8e82b2 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 19 Dec 2015 11:44:12 +0800 Subject: Parse misc nc drill files --- gerber/excellon.py | 68 ++++++++++++++++++++++---- gerber/excellon_settings.py | 105 +++++++++++++++++++++++++++++++++++++++ gerber/excellon_statements.py | 26 +++++++++- gerber/excellon_tool.py | 111 ++++++++++++++++++++++++++++++++++++++++++ gerber/primitives.py | 2 +- 5 files changed, 300 insertions(+), 12 deletions(-) create mode 100644 gerber/excellon_settings.py create mode 100644 gerber/excellon_tool.py 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 + +# 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 + +# 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[0-9/.]+)\s+(?PP|N)\s+T(?P[0-9]{2})\s+(?P[0-9/.]+)\s+(?P[0-9/.]+)') + allegro_comment_mils = re.compile('Holesize (?P[0-9]{1,2})\. = (?P[0-9/.]+) Tolerance = \+(?P[0-9/.]+)/-(?P[0-9/.]+) (?P(PLATED)|(NON_PLATED)) MILS Quantity = [0-9]+') + allegro_comment_mm = re.compile('Holesize (?P[0-9]{1,2})\. = (?P[0-9/.]+) Tolerance = \+(?P[0-9/.]+)/-(?P[0-9/.]+) (?P(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 -- cgit