Add CNC base classes and settings class

refactor
Hamilton Kibbe 2014-09-29 13:35:10 -04:00
rodzic c60a858c9c
commit 1cb7856e88
3 zmienionych plików z 206 dodań i 23 usunięć

117
gerber/cnc.py 100644
Wyświetl plik

@ -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)

Wyświetl plik

@ -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):

Wyświetl plik

@ -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)