Fix parsing for multiline ipc-d-356 records

This commit is contained in:
Hamilton Kibbe 2015-03-05 22:42:42 -05:00
parent c40683b6a2
commit 68619d4d5a
8 changed files with 218 additions and 55 deletions

View file

@ -18,7 +18,8 @@
import math
import re
from .cam import FileSettings
from .cam import CamFile, FileSettings
from .primitives import TestRecord
# Net Name Variables
_NNAME = re.compile(r'^NNAME\d+$')
@ -44,7 +45,7 @@ def read(filename):
return IPC_D_356.from_file(filename)
class IPC_D_356(object):
class IPC_D_356(CamFile):
@classmethod
def from_file(self, filename):
@ -52,10 +53,12 @@ class IPC_D_356(object):
return p.parse(filename)
def __init__(self, statements, settings):
def __init__(self, statements, settings, primitives=None):
self.statements = statements
self.units = settings.units
self.angle_units = settings.angle_units
self.primitives = [TestRecord((rec.x_coord, rec.y_coord), rec.net_name,
rec.access) for rec in self.test_records]
@property
def settings(self):
@ -98,6 +101,19 @@ class IPC_D_356(object):
else:
return None
def render(self, ctx, layer='both', filename=None):
for p in self.primitives:
if layer == 'both' and p.layer in ('top', 'bottom', 'both'):
ctx.render(p)
elif layer == 'top' and p.layer in ('top', 'both'):
ctx.render(p)
elif layer == 'bottom' and p.layer in ('bottom', 'both'):
ctx.render(p)
if filename is not None:
ctx.dump(filename)
class IPC_D_356_Parser(object):
# TODO: Allow multi-line statements (e.g. Altium board edge)
def __init__(self):
@ -112,51 +128,68 @@ class IPC_D_356_Parser(object):
def parse(self, filename):
with open(filename, 'r') as f:
oldline = ''
for line in f:
if line[0] == 'C':
# Comment
self.statements.append(IPC356_Comment.from_line(line))
elif line[0] == 'P':
# Parameter
p = IPC356_Parameter.from_line(line)
if p.parameter == 'UNITS':
if p.value in ('CUST', 'CUST 0'):
self.units = 'inch'
self.angle_units = 'degrees'
elif p.value == 'CUST 1':
self.units = 'metric'
self.angle_units = 'degrees'
elif p.value == 'CUST 2':
self.units = 'inch'
self.angle_units = 'radians'
self.statements.append(p)
if _NNAME.match(p.parameter):
# Add to list of net name variables
self.nnames[p.parameter] = p.value
elif line[0] == '3' and line[2] == '7':
# Test Record
record = IPC356_TestRecord.from_line(line, self.settings)
# Substitute net name variables
net = record.net_name
if (_NNAME.match(net) and net in self.nnames.keys()):
record.net_name = self.nnames[record.net_name]
self.statements.append(record)
elif line[0:3] == '389':
# Altium Board Edge Info
self.statements.append(IPC356_BoardEdge.from_line(line, self.settings))
elif line[0] == '9':
self.multiline = False
self.statements.append(IPC356_EndOfFile())
# Check for existing multiline data...
if oldline != '':
if len(line) and line[0] == '0':
oldline = oldline.rstrip('\r\n') + line[3:].rstrip()
else:
self._parse_line(oldline)
oldline = line
else:
oldline = line
self._parse_line(oldline)
return IPC_D_356(self.statements, self.settings)
def _parse_line(self, line):
if not len(line):
return
if line[0] == 'C':
# Comment
self.statements.append(IPC356_Comment.from_line(line))
elif line[0] == 'P':
# Parameter
p = IPC356_Parameter.from_line(line)
if p.parameter == 'UNITS':
if p.value in ('CUST', 'CUST 0'):
self.units = 'inch'
self.angle_units = 'degrees'
elif p.value == 'CUST 1':
self.units = 'metric'
self.angle_units = 'degrees'
elif p.value == 'CUST 2':
self.units = 'inch'
self.angle_units = 'radians'
self.statements.append(p)
if _NNAME.match(p.parameter):
# Add to list of net name variables
self.nnames[p.parameter] = p.value
elif line[0] == '9':
self.statements.append(IPC356_EndOfFile())
elif line[0:3] in ('317', '327', '367'):
# Test Record
record = IPC356_TestRecord.from_line(line, self.settings)
# Substitute net name variables
net = record.net_name
if (_NNAME.match(net) and net in self.nnames.keys()):
record.net_name = self.nnames[record.net_name]
self.statements.append(record)
elif line[0:3] == '379':
# Net Adjacency Info
pass
elif line[0:3] == '389':
# Altium Board Edge Info
self.statements.append(IPC356_BoardEdge.from_line(line, self.settings))
class IPC356_Comment(object):
@classmethod
def from_line(cls, line):
@ -302,6 +335,19 @@ class IPC356_BoardEdge(object):
return '<IPC-D-356 Board Edge Definition>'
class IPC356_Adjacency(object):
@classmethod
def from_line(cls, line):
nets = line.strip().split()[1:]
return cls(nets)
def __init__(self, nets):
self.nets = nets
def __repr__(self):
return '<IPC-D-356 Adjacency Record>'
class IPC356_EndOfFile(object):
def __init__(self):

View file

@ -713,8 +713,7 @@ class Donut(Primitive):
"""
def __init__(self, position, shape, inner_diameter, outer_diameter, **kwargs):
super(Donut, self).__init__(**kwargs)
if len(position) != 2:
raise TypeError('Position must be a tuple (n=2) of coordinates')
validate_coordinates(position)
self.position = position
if shape not in ('round', 'square', 'hexagon', 'octagon'):
raise ValueError('Valid shapes are round, square, hexagon or octagon')
@ -731,7 +730,6 @@ class Donut(Primitive):
self.width = 0.5 * math.sqrt(3.) * outer_diameter
self.height = outer_diameter
@property
def lower_left(self):
return (self.position[0] - (self.width / 2.),
@ -755,14 +753,56 @@ class Donut(Primitive):
self.width = inch(self.width)
self.height = inch(self.height)
self.inner_diameter = inch(self.inner_diameter)
self.outer_diaemter = inch(self.outer_diameter)
self.outer_diameter = inch(self.outer_diameter)
def to_metric(self):
self.position = tuple(map(metric, self.position))
self.width = metric(self.width)
self.height = metric(self.height)
self.inner_diameter = metric(self.inner_diameter)
self.outer_diaemter = metric(self.outer_diameter)
self.outer_diameter = metric(self.outer_diameter)
def offset(self, x_offset=0, y_offset=0):
self.position = tuple(map(add, self.position, (x_offset, y_offset)))
class SquareRoundDonut(Primitive):
""" A Square with a circular cutout in the center
"""
def __init__(self, position, inner_diameter, outer_diameter, **kwargs):
super(SquareRoundDonut, self).__init__(**kwargs)
validate_coordinates(position)
self.position = position
if inner_diameter >= outer_diameter:
raise ValueError('Outer diameter must be larger than inner diameter.')
self.inner_diameter = inner_diameter
self.outer_diameter = outer_diameter
@property
def lower_left(self):
return tuple([c - self.outer_diameter / 2. for c in self.position])
@property
def upper_right(self):
return tuple([c + self.outer_diameter / 2. for c in self.position])
@property
def bounding_box(self):
min_x = self.lower_left[0]
max_x = self.upper_right[0]
min_y = self.lower_left[1]
max_y = self.upper_right[1]
return ((min_x, max_x), (min_y, max_y))
def to_inch(self):
self.position = tuple(map(inch, self.position))
self.inner_diameter = inch(self.inner_diameter)
self.outer_diameter = inch(self.outer_diameter)
def to_metric(self):
self.position = tuple(map(metric, self.position))
self.inner_diameter = metric(self.inner_diameter)
self.outer_diameter = metric(self.outer_diameter)
def offset(self, x_offset=0, y_offset=0):
self.position = tuple(map(add, self.position, (x_offset, y_offset)))
@ -773,8 +813,7 @@ class Drill(Primitive):
"""
def __init__(self, position, diameter):
super(Drill, self).__init__('dark')
if len(position) != 2:
raise TypeError('Position must be a tuple (n=2) of coordinates')
validate_coordinates(position)
self.position = position
self.diameter = diameter
@ -801,3 +840,13 @@ class Drill(Primitive):
def offset(self, x_offset=0, y_offset=0):
self.position = tuple(map(add, self.position, (x_offset, y_offset)))
class TestRecord(Primitive):
""" Netlist Test record
"""
def __init__(self, position, net_name, layer, **kwargs):
super(TestRecord, self).__init__(**kwargs)
validate_coordinates(position)
self.position = position
self.net_name = net_name
self.layer = layer

View file

@ -104,7 +104,7 @@ class GerberCairoContext(GerberContext):
self.ctx.fill()
def _render_circle(self, circle, color):
center = map(mul, circle.position, self.scale)
center = tuple(map(mul, circle.position, self.scale))
self.ctx.set_source_rgba(*color, alpha=self.alpha)
self.ctx.set_line_width(0)
self.ctx.arc(*center, radius=circle.radius * SCALE, angle1=0, angle2=2 * math.pi)
@ -126,5 +126,15 @@ class GerberCairoContext(GerberContext):
def _render_drill(self, circle, color):
self._render_circle(circle, color)
def _render_test_record(self, primitive, color):
self.ctx.select_font_face('monospace', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
self.ctx.set_font_size(200)
self._render_circle(Circle(primitive.position, 0.01), color)
self.ctx.set_source_rgb(*color)
self.ctx.move_to(*[SCALE * (coord + 0.01) for coord in primitive.position])
self.ctx.scale(1, -1)
self.ctx.show_text(primitive.net_name)
self.ctx.scale(1, -1)
def dump(self, filename):
self.surface.write_to_png(filename)

View file

@ -138,9 +138,11 @@ class GerberContext(object):
elif isinstance(primitive, Obround):
self._render_obround(primitive, color)
elif isinstance(primitive, Polygon):
self._render_polygon(Polygon, color)
self._render_polygon(primitive, color)
elif isinstance(primitive, Drill):
self._render_drill(primitive, self.drill_color)
elif isinstance(primitive, TestRecord):
self._render_test_record(primitive, color)
else:
return
@ -168,3 +170,5 @@ class GerberContext(object):
def _render_drill(self, primitive, color):
pass
def _render_test_record(self, primitive, color):
pass

View file

@ -111,4 +111,5 @@ P NNAME1 A_REALLY_LONG_NET_NAME
327VCC U4 -8 A01X 8396Y 3850X 394Y 500R 0
327NNAME1 NA -69 A01X 8396Y 3850X 394Y 500R 0
389BOARD_EDGE X0Y0 X22500 Y15000 X0
089 X1300Y240
999

View file

@ -25,7 +25,7 @@ def test_parser():
assert_equal(len(ipcfile.vias), 14)
assert_equal(ipcfile.test_records[-1].net_name, 'A_REALLY_LONG_NET_NAME')
assert_equal(set(ipcfile.board_outline),
{(0., 0.), (2.25, 0.), (2.25, 1.5), (0., 1.5)})
{(0., 0.), (2.25, 0.), (2.25, 1.5), (0., 1.5), (0.13, 0.024)})
def test_comment():
c = IPC356_Comment('Layer Stackup:')

View file

@ -681,13 +681,13 @@ def test_donut_conversion():
d.to_inch()
assert_equal(d.position, (0.1, 1.0))
assert_equal(d.inner_diameter, 10.0)
assert_equal(d.outer_diaemter, 100.0)
assert_equal(d.outer_diameter, 100.0)
d = Donut((0.1, 1.0), 'round', 10.0, 100.0)
d.to_metric()
assert_equal(d.position, (2.54, 25.4))
assert_equal(d.inner_diameter, 254.0)
assert_equal(d.outer_diaemter, 2540.0)
assert_equal(d.outer_diameter, 2540.0)
def test_donut_offset():
d = Donut((0, 0), 'round', 1, 10)

View file

@ -26,6 +26,9 @@ files.
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
# License:
from math import radians, sin, cos
from operator import sub
MILLIMETERS_PER_INCH = 25.4
def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'):
@ -238,7 +241,57 @@ def validate_coordinates(position):
def metric(value):
""" Convert inch value to millimeters
Parameters
----------
value : float
A value in inches.
Returns
-------
value : float
The equivalent value expressed in millimeters.
"""
return value * MILLIMETERS_PER_INCH
def inch(value):
""" Convert millimeter value to inches
Parameters
----------
value : float
A value in millimeters.
Returns
-------
value : float
The equivalent value expressed in inches.
"""
return value / MILLIMETERS_PER_INCH
def rotate_point(point, angle, center=(0.0, 0.0)):
""" Rotate a point about another point.
Parameters
-----------
point : tuple(<float>, <float>)
Point to rotate about origin or center point
angle : float
Angle to rotate the point [degrees]
center : tuple(<float>, <float>)
Coordinates about which the point is rotated. Defaults to the origin.
Returns
-------
rotated_point : tuple(<float>, <float>)
`point` rotated about `center` by `angle` degrees.
"""
angle = radians(angle)
xdelta, ydelta = tuple(map(sub, point, center))
x = center[0] + (cos(angle) * xdelta) - (sin(angle) * ydelta)
y = center[1] + (sin(angle) * xdelta) - (cos(angle) * ydelta)
return (x, y)