summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--gerber/cnc.py117
-rwxr-xr-xgerber/excellon.py91
-rw-r--r--gerber/gerber.py21
3 files changed, 206 insertions, 23 deletions
diff --git a/gerber/cnc.py b/gerber/cnc.py
new file mode 100644
index 0000000..a7f3b85
--- /dev/null
+++ b/gerber/cnc.py
@@ -0,0 +1,117 @@
+#! /usr/bin/env python
+# -*- 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
+
+# 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.
+"""
+gerber.cnc
+============
+**CNC file classes**
+
+This module provides common base classes for Excellon/Gerber CNC files
+"""
+
+
+class FileSettings(object):
+ """ CNC File Settings
+
+ Provides a common representation of gerber/excellon file settings
+ """
+ def __init__(self, notation='absolute', units='inch',
+ zero_suppression='trailing', format=(2,5)):
+ if notation not in ['absolute', 'incremental']:
+ raise ValueError('Notation must be either absolute or incremental')
+ self.notation = notation
+
+ if units not in ['inch', 'metric']:
+ raise ValueError('Units must be either inch or metric')
+ self.units = units
+
+ if zero_suppression not in ['leading', 'trailing']:
+ raise ValueError('Zero suppression must be either leading or \
+ trailling')
+ self.zero_suppression = zero_suppression
+
+ if len(format) != 2:
+ raise ValueError('Format must be a tuple(n=2) of integers')
+ self.format = format
+
+ def __getitem__(self, key):
+ if key == 'notation':
+ return self.notation
+ elif key == 'units':
+ return self.units
+ elif key =='zero_suppression':
+ return self.zero_suppression
+ elif key == 'format':
+ return self.format
+ else:
+ raise KeyError()
+
+class CncFile(object):
+ """ Base class for Gerber/Excellon files.
+
+ Provides a common set of settings parameters.
+
+ Parameters
+ ----------
+ settings : FileSettings
+ The current file configuration.
+
+ filename : string
+ Name of the file that this CncFile represents.
+
+ Attributes
+ ----------
+ settings : FileSettings
+ File settings as a FileSettings object
+
+ notation : string
+ File notation setting. May be either 'absolute' or 'incremental'
+
+ units : string
+ File units setting. May be 'inch' or 'metric'
+
+ zero_suppression : string
+ File zero-suppression setting. May be either 'leading' or 'trailling'
+
+ format : tuple (<int>, <int>)
+ File decimal representation format as a tuple of (integer digits,
+ decimal digits)
+ """
+
+ def __init__(self, settings=None, filename=None):
+ if settings is not None:
+ self.notation = settings['notation']
+ self.units = settings['units']
+ self.zero_suppression = settings['zero_suppression']
+ self.format = settings['format']
+ else:
+ self.notation = 'absolute'
+ self.units = 'inch'
+ self.zero_suppression = 'trailing'
+ self.format = (2,5)
+ self.filename = filename
+
+ @property
+ def settings(self):
+ """ File settings
+
+ Returns
+ -------
+ settings : FileSettings (dict-like)
+ A FileSettings object with the specified configuration.
+ """
+ return FileSettings(self.notation, self.units, self.zero_suppression,
+ self.format)
diff --git a/gerber/excellon.py b/gerber/excellon.py
index d92d57c..5cb33ad 100755
--- a/gerber/excellon.py
+++ b/gerber/excellon.py
@@ -2,6 +2,7 @@
import re
from itertools import tee, izip
from .utils import parse_gerber_value
+from .cnc import CncFile, FileSettings
def read(filename):
@@ -10,7 +11,7 @@ def read(filename):
return ExcellonParser().parse(filename)
-class ExcellonFile(object):
+class ExcellonFile(CncFile):
""" A class representing a single excellon file
The ExcellonFile class represents a single excellon file.
@@ -34,11 +35,10 @@ class ExcellonFile(object):
either 'inch' or 'metric'.
"""
- def __init__(self, tools, hits, settings, filename):
+ def __init__(self, tools, hits, settings, filename=None):
+ super(ExcellonFile, self).__init__(settings, filename)
self.tools = tools
self.hits = hits
- self.settings = settings
- self.filename = filename
def report(self):
""" Print drill report
@@ -53,11 +53,67 @@ class ExcellonFile(object):
ctx.dump(filename)
-class Tool(object):
+class ExcellonTool(object):
""" Excellon Tool class
+
+ Parameters
+ ----------
+ settings : FileSettings (dict-like)
+ File-wide settings.
+
+ kwargs : dict-like
+ Tool settings from the excellon statement. Valid keys are:
+ diameter : Tool diameter [expressed in file units]
+ rpm : Tool RPM
+ feed_rate : Z-axis tool feed rate
+ retract_rate : Z-axis tool retraction rate
+ max_hit_count : Number of hits allowed before a tool change
+ depth_offset : Offset of tool depth from tip of tool.
+
+ Attributes
+ ----------
+ number : integer
+ Tool number from the excellon file
+
+ diameter : float
+ Tool diameter in file units
+
+ rpm : float
+ Tool RPM
+
+ feed_rate : float
+ Tool Z-axis feed rate.
+
+ retract_rate : float
+ Tool Z-axis retract rate
+
+ depth_offset : float
+ Offset of depth measurement from tip of tool
+
+ max_hit_count : integer
+ Maximum number of tool hits allowed before a tool change
+
+ hit_count : integer
+ Number of tool hits in excellon file.
"""
+
@classmethod
def from_line(cls, line, settings):
+ """ Create a Tool from an excellon gile tool definition line.
+
+ Parameters
+ ----------
+ line : string
+ Tool definition line from an excellon file.
+
+ settings : FileSettings (dict-like)
+ Excellon file-wide settings
+
+ Returns
+ -------
+ tool : Tool
+ An ExcellonTool representing the tool defined in `line`
+ """
commands = re.split('([BCFHSTZ])', line)[1:]
commands = [(command, value) for command, value in pairwise(commands)]
args = {}
@@ -89,13 +145,19 @@ class Tool(object):
self.max_hit_count = kwargs.get('max_hit_count')
self.depth_offset = kwargs.get('depth_offset')
self.units = settings.get('units', 'inch')
+ self.hit_count = 0
+
+ def _hit(self):
+ self.hit_count += 1
def __repr__(self):
unit = 'in.' if self.units == 'inch' else 'mm'
- return '<Tool %d: %0.3f%s dia.>' % (self.number, self.diameter, unit)
+ return '<ExcellonTool %d: %0.3f%s dia.>' % (self.number, self.diameter, unit)
class ExcellonParser(object):
+ """ Excellon File Parser
+ """
def __init__(self, ctx=None):
self.ctx = ctx
self.notation = 'absolute'
@@ -115,13 +177,11 @@ class ExcellonParser(object):
with open(filename, 'r') as f:
for line in f:
self._parse(line)
- settings = {'notation': self.notation, 'units': self.units,
- 'zero_suppression': self.zero_suppression,
- 'format': self.format}
- return ExcellonFile(self.tools, self.hits, settings, filename)
+ return ExcellonFile(self.tools, self.hits, self._settings(), filename)
def dump(self, filename):
- self.ctx.dump(filename)
+ if self.ctx is not None:
+ self.ctx.dump(filename)
def _parse(self, line):
if 'M48' in line:
@@ -159,7 +219,7 @@ class ExcellonParser(object):
# tool definition
if line[0] == 'T' and self.state == 'HEADER':
- tool = Tool.from_line(line, self._settings())
+ tool = ExcellonTool.from_line(line, self._settings())
self.tools[tool.number] = tool
elif line[0] == 'T' and self.state != 'HEADER':
@@ -187,13 +247,16 @@ class ExcellonParser(object):
self.pos[1] += y
if self.state == 'DRILL':
self.hits.append((self.active_tool, self.pos))
+ self.active_tool._hit()
if self.ctx is not None:
self.ctx.drill(self.pos[0], self.pos[1],
self.active_tool.diameter)
def _settings(self):
- return {'units': self.units, 'zero_suppression': self.zero_suppression,
- 'format': self.format}
+ return FileSettings(units=self.units, format=self.format,
+ zero_suppression=self.zero_suppression,
+ notation=self.notation)
+
def pairwise(iterator):
diff --git a/gerber/gerber.py b/gerber/gerber.py
index 949037b..eb5821c 100644
--- a/gerber/gerber.py
+++ b/gerber/gerber.py
@@ -27,6 +27,9 @@ This module provides an RS-274-X class and parser
import re
import json
from .statements import *
+from .cnc import CncFile, FileSettings
+
+
def read(filename):
@@ -35,7 +38,7 @@ def read(filename):
return GerberParser().parse(filename)
-class GerberFile(object):
+class GerberFile(CncFile):
""" A class representing a single gerber file
The GerberFile class represents a single gerber file.
@@ -68,9 +71,8 @@ class GerberFile(object):
"""
def __init__(self, statements, settings, filename=None):
- self.filename = filename
+ super(GerberFile, self).__init__(settings, filename)
self.statements = statements
- self.settings = settings
@property
def comments(self):
@@ -90,7 +92,8 @@ class GerberFile(object):
def bounds(self):
xbounds = [0.0, 0.0]
ybounds = [0.0, 0.0]
- for stmt in [stmt for stmt in self.statements if isinstance(stmt, CoordStmt)]:
+ for stmt in [stmt for stmt in self.statements
+ if isinstance(stmt, CoordStmt)]:
if stmt.x is not None and stmt.x < xbounds[0]:
xbounds[0] = stmt.x
if stmt.x is not None and stmt.x > xbounds[1]:
@@ -169,7 +172,7 @@ class GerberParser(object):
EOF_STMT = re.compile(r"(?P<eof>M02)\*")
def __init__(self):
- self.settings = {}
+ self.settings = FileSettings()
self.statements = []
def parse(self, filename):
@@ -240,13 +243,13 @@ class GerberParser(object):
if param:
if param["param"] == "FS":
stmt = FSParamStmt.from_dict(param)
- self.settings = {'zero_suppression': stmt.zero_suppression,
- 'format': stmt.format,
- 'notation': stmt.notation}
+ self.settings.zero_suppression = stmt.zero_suppression
+ self.settings.format = stmt.format
+ self.settings.notation = stmt.notation
yield stmt
elif param["param"] == "MO":
stmt = MOParamStmt.from_dict(param)
- self.settings['units'] = stmt.mode
+ self.settings.units = stmt.mode
yield stmt
elif param["param"] == "IP":
yield IPParamStmt.from_dict(param)