import io import math import os import string import zipfile from datetime import datetime from xml.etree import ElementTree from .common import EcadParser, Component, BoundingBox from .svgpath import Arc from ..core.fontparser import FontParser class FusionEagleParser(EcadParser): TOP_COPPER_LAYER = '1' BOT_COPPER_LAYER = '16' TOP_PLACE_LAYER = '21' BOT_PLACE_LAYER = '22' TOP_NAMES_LAYER = '25' BOT_NAMES_LAYER = '26' DIMENSION_LAYER = '20' TOP_DOCU_LAYER = '51' BOT_DOCU_LAYER = '52' def __init__(self, file_name, config, logger): super(FusionEagleParser, self).__init__(file_name, config, logger) self.config = config self.font_parser = FontParser() self.min_via_w = 1e-3 self.pcbdata = { 'drawings': { 'silkscreen': { 'F': [], 'B': [] }, 'fabrication': { 'F': [], 'B': [] } }, 'edges': [], 'footprints': [], 'font_data': {} } self.components = [] def _parse_pad_nets(self, signals): elements = {} for signal in signals.iter('signal'): net = signal.attrib['name'] for c in signal.iter('contactref'): e = c.attrib['element'] if e not in elements: elements[e] = {} elements[e][c.attrib['pad']] = net self.elements_pad_nets = elements @staticmethod def _radian(ux, uy, vx, vy): dot = ux * vx + uy * vy mod = math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy)) rad = math.acos(dot / mod) if ux * vy - uy * vx < 0.0: rad = -rad return rad def _curve_to_svgparams(self, el, x=0, y=0, angle=0, mirrored=False): _x1 = float(el.attrib['x1']) _x2 = float(el.attrib['x2']) _y1 = -float(el.attrib['y1']) _y2 = -float(el.attrib['y2']) dx1, dy1 = self._rotate(_x1, _y1, -angle, mirrored) dx2, dy2 = self._rotate(_x2, _y2, -angle, mirrored) x1, y1 = x + dx1, -y + dy1 x2, y2 = x + dx2, -y + dy2 chord = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) theta = float(el.attrib['curve']) theta = -theta if mirrored else theta r = abs(0.5 * chord / math.sin(math.radians(theta) / 2)) la = 0 if abs(theta) < 180 else 1 sw = 0 if theta > 0 else 1 return { 'x1': x1, 'y1': y1, 'r': r, 'la': la, 'sw': sw, 'x2': x2, 'y2': y2 } def _curve_to_svgpath(self, el, x=0, y=0, angle=0, mirrored=False): p = self._curve_to_svgparams(el, x, y, angle, mirrored) return 'M {x1} {y1} A {r} {r} 0 {la} {sw} {x2} {y2}'.format(**p) @staticmethod class Rot: def __init__(self, rot_string): if not rot_string: self.mirrored = False self.spin = False self.angle = 0 return self.mirrored = 'M' in rot_string self.spin = 'S' in rot_string self.angle = float(''.join(d for d in rot_string if d in string.digits + '.')) def __repr__(self): return self.__str__() def __str__(self): return "Mirrored: {0}, Spin: {1}, Angle: {2}".format(self.mirrored, self.spin, self.angle) def _rectangle_vertices(self, el): # Note: Eagle specifies a rectangle using opposing corners # (x1, y1) = lower-left and (x2, y2) = upper-right) and *optionally* # a rotation angle. The size of the rectangle is defined by the # corners irrespective of rotation angle, and then it is rotated # about its own center point. _x1 = float(el.attrib['x1']) _x2 = float(el.attrib['x2']) _y1 = -float(el.attrib['y1']) _y2 = -float(el.attrib['y2']) # Center of rectangle xc = (_x1 + _x2) / 2 yc = (_y1 + _y2) / 2 # Vertices of rectangle relative to its center, un-rotated _dv_c = [ (_x1 - xc, _y1 - yc), (_x2 - xc, _y1 - yc), (_x2 - xc, _y2 - yc), (_x1 - xc, _y2 - yc) ] elr = self.Rot(el.get('rot')) # Rotate the rectangle about its center dv_c = [self._rotate(_x, _y, -elr.angle, elr.mirrored) for (_x, _y) in _dv_c] # Map vertices to position relative to component origin, un-rotated return [(_x + xc, _y + yc) for (_x, _y) in dv_c] def _add_drawing(self, el): layer_dest = { self.DIMENSION_LAYER: self.pcbdata['edges'], self.TOP_PLACE_LAYER: self.pcbdata['drawings']['silkscreen']['F'], self.BOT_PLACE_LAYER: self.pcbdata['drawings']['silkscreen']['B'], self.TOP_NAMES_LAYER: self.pcbdata['drawings']['silkscreen']['F'], self.BOT_NAMES_LAYER: self.pcbdata['drawings']['silkscreen']['B'] } if ("layer" in el.attrib) and (el.attrib['layer'] in layer_dest): dwg = None if el.tag == 'wire': dwg = {'width': float(el.attrib['width'])} if 'curve' in el.attrib: dwg['type'] = 'arc' dwg['svgpath'] = self._curve_to_svgpath(el) else: dwg['type'] = 'segment' dwg['start'] = [ float(el.attrib['x1']), -float(el.attrib['y1']) ] dwg['end'] = [ float(el.attrib['x2']), -float(el.attrib['y2']) ] elif el.tag == 'text': # Text is not currently supported (except refdes) # due to lack of Eagle font data pass elif el.tag == 'circle': dwg = { 'type': 'circle', 'start': [float(el.attrib['x']), -float(el.attrib['y'])], 'radius': float(el.attrib['radius']), 'width': float(el.attrib['width']) } elif el.tag in ['polygonshape', 'polygon']: dwg = { 'type': 'polygon', 'pos': [0, 0], 'angle': 0, 'polygons': [] } segs = el if el.tag == 'polygon' \ else el.find('polygonoutlinesegments') polygon = FusionEagleParser._segments_to_polygon(segs) dwg['polygons'].append(polygon) elif el.tag == 'rectangle': vertices = self._rectangle_vertices(el) dwg = { 'type': 'polygon', 'pos': [0, 0], 'angle': 0, 'polygons': [[list(v) for v in vertices]] } if dwg: layer_dest[el.attrib['layer']].append(dwg) def _add_track(self, el, net): if el.tag == 'via' or ( el.tag == 'wire' and el.attrib['layer'] in [self.TOP_COPPER_LAYER, self.BOT_COPPER_LAYER]): trk = {} if self.config.include_nets: trk['net'] = net if el.tag == 'wire': dest = self.pcbdata['tracks']['F'] \ if el.attrib['layer'] == self.TOP_COPPER_LAYER\ else self.pcbdata['tracks']['B'] if 'curve' in el.attrib: trk['width'] = float(el.attrib['width']) # Get SVG parameters for the curve p = self._curve_to_svgparams(el) start = complex(p['x1'], p['y1']) end = complex(p['x2'], p['y2']) radius = complex(p['r'], p['r']) large_arc = bool(p['la']) sweep = bool(p['sw']) # Pass SVG parameters to get center parameters arc = Arc(start, radius, 0, large_arc, sweep, end) # Create arc track from center parameters trk['center'] = [arc.center.real, arc.center.imag] trk['radius'] = radius.real if arc.delta < 0: trk['startangle'] = arc.theta + arc.delta trk['endangle'] = arc.theta else: trk['startangle'] = arc.theta trk['endangle'] = arc.theta + arc.delta dest.append(trk) else: trk['start'] = [ float(el.attrib['x1']), -float(el.attrib['y1']) ] trk['end'] = [ float(el.attrib['x2']), -float(el.attrib['y2']) ] trk['width'] = float(el.attrib['width']) dest.append(trk) elif el.tag == 'via': trk['start'] = [float(el.attrib['x']), -float(el.attrib['y'])] trk['end'] = trk['start'] trk['width'] = float(el.attrib['drill']) + 2 * self.min_via_w \ if 'diameter' not in el.attrib else float( el.attrib['diameter']) if float(el.attrib['drill']) >= self.min_drill_via_untented: trk['drillsize'] = float(el.attrib['drill']) self.pcbdata['tracks']['F'].append(trk) self.pcbdata['tracks']['B'].append(trk) def _calculate_footprint_bbox(self, package, x, y, angle, mirrored): _angle = angle if not mirrored else -angle layers = [ self.TOP_PLACE_LAYER, self.BOT_PLACE_LAYER, self.TOP_DOCU_LAYER, self.BOT_DOCU_LAYER ] xmax, ymax = -float('inf'), -float('inf') xmin, ymin = float('inf'), float('inf') for el in package.iter('wire'): if el.tag == 'wire' and el.attrib['layer'] in layers: xmax = max(xmax, max(float(el.attrib['x1']), float(el.attrib['x2']))) ymax = max(ymax, max(float(el.attrib['y1']), float(el.attrib['y2']))) xmin = min(xmin, min(float(el.attrib['x1']), float(el.attrib['x2']))) ymin = min(ymin, min(float(el.attrib['y1']), float(el.attrib['y2']))) for el in package.iter(): if el.tag in ['smd', 'pad']: elx, ely = float(el.attrib['x']), float(el.attrib['y']) if el.tag == 'smd': dx, dy = abs(float(el.attrib['dx'])) / 2, abs( float(el.attrib['dy'])) / 2 else: d = el.get('diameter') if d is None: diameter = float(el.get('drill')) + 2 * self.min_via_w else: diameter = float(d) dx, dy = diameter / 2, diameter / 2 xmax, ymax = max(xmax, elx + dx), max(ymax, ely + dy) xmin, ymin = min(xmin, elx - dx), min(ymin, ely - dy) if not math.isinf(xmin): if mirrored: xmin, xmax = -xmax, -xmin dx, dy = self._rotate(xmin, ymax, _angle) sx = abs(xmax - xmin) sy = abs(ymax - ymin) else: dx, dy = 0, 0 sx, sy = 0, 0 return { 'pos': [x + dx, -y - dy], 'angle': _angle, 'relpos': [0, 0], 'size': [sx, sy] } def _footprint_pads(self, package, x, y, angle, mirrored, refdes): pads = [] element_pad_nets = self.elements_pad_nets.get(refdes) pin1_allocated = False for el in package.iter(): if el.tag == 'pad': elx = float(el.attrib['x']) ely = -float(el.attrib['y']) drill = float(el.attrib['drill']) dx, dy = self._rotate(elx, ely, -angle, mirrored) diameter = drill + 2 * self.min_via_w \ if 'diameter' not in el.attrib \ else float(el.attrib['diameter']) pr = self.Rot(el.get('rot')) if mirrored ^ pr.mirrored: pad_angle = -angle - pr.angle else: pad_angle = angle + pr.angle pad = { 'layers': ['F', 'B'], 'pos': [x + dx, -y + dy], 'angle': pad_angle, 'type': 'th', 'drillshape': 'circle', 'drillsize': [ drill, drill ] } if el.get('name') in ['1', 'A', 'A1', 'P1', 'PAD1'] and \ not pin1_allocated: pad['pin1'] = 1 pin1_allocated = True if 'shape' not in el.attrib or el.attrib['shape'] == 'round': pad['shape'] = 'circle' pad['size'] = [diameter, diameter] elif el.attrib['shape'] == 'square': pad['shape'] = 'rect' pad['size'] = [diameter, diameter] elif el.attrib['shape'] == 'octagon': pad['shape'] = 'chamfrect' pad['size'] = [diameter, diameter] pad['radius'] = 0 pad['chamfpos'] = 0b1111 # all corners pad['chamfratio'] = 0.333 elif el.attrib['shape'] == 'long': pad['shape'] = 'roundrect' pad['radius'] = diameter / 2 pad['size'] = [2 * diameter, diameter] elif el.attrib['shape'] == 'offset': pad['shape'] = 'roundrect' pad['radius'] = diameter / 2 pad['size'] = [2 * diameter, diameter] pad['offset'] = [diameter / 2, 0] elif el.attrib['shape'] == 'slot': pad['shape'] = 'roundrect' pad['radius'] = diameter / 2 slot_length = float(el.attrib['slotLength']) pad['size'] = [slot_length + diameter / 2, diameter] pad['drillshape'] = 'oblong' pad['drillsize'] = [slot_length, drill] else: self.logger.info( "Unsupported footprint pad shape %s, skipping", el.attrib['shape']) if self.config.include_nets and element_pad_nets is not None: net = element_pad_nets.get(el.attrib['name']) if net is not None: pad['net'] = net pads.append(pad) elif el.tag == 'smd': layer = el.attrib['layer'] if layer == self.TOP_COPPER_LAYER and not mirrored or \ layer == self.BOT_COPPER_LAYER and mirrored: layers = ['F'] elif layer == self.TOP_COPPER_LAYER and mirrored or \ layer == self.BOT_COPPER_LAYER and not mirrored: layers = ['B'] else: self.logger.error('Unable to determine layer for ' '{0} pad {1}'.format(refdes, el.attrib['name'])) layers = None if layers is not None: elx = float(el.attrib['x']) ely = -float(el.attrib['y']) dx, dy = self._rotate(elx, ely, -angle, mirrored) pr = self.Rot(el.get('rot')) if mirrored ^ pr.mirrored: pad_angle = -angle - pr.angle else: pad_angle = angle + pr.angle pad = {'layers': layers, 'pos': [x + dx, -y + dy], 'size': [ float(el.attrib['dx']), float(el.attrib['dy']) ], 'angle': pad_angle, 'type': 'smd', } if el.get('name') in ['1', 'A', 'A1', 'P1', 'PAD1'] and \ not pin1_allocated: pad['pin1'] = 1 pin1_allocated = True if 'roundness' not in el.attrib: pad['shape'] = 'rect' else: pad['shape'] = 'roundrect' pad['radius'] = (float(el.attrib['roundness']) / 100) \ * float(el.attrib['dy']) / 2 if self.config.include_nets and \ element_pad_nets is not None: net = element_pad_nets.get(el.attrib['name']) if net is not None: pad['net'] = net pads.append(pad) return pads @staticmethod def _rotate(x, y, angle, mirrored=False): sin = math.sin(math.radians(angle)) cos = math.cos(math.radians(angle)) xr = x * cos - y * sin yr = y * cos + x * sin if mirrored: return -xr, yr else: return xr, yr def _process_footprint(self, package, x, y, angle, mirrored, populate): for el in package.iter(): if el.tag in ['wire', 'rectangle', 'circle', 'hole', 'polygonshape', 'polygon', 'hole']: if el.tag == 'hole': dwg_layer = self.pcbdata['edges'] elif el.attrib['layer'] in [self.TOP_PLACE_LAYER, self.BOT_PLACE_LAYER]: dwg_layer = self.pcbdata['drawings']['silkscreen'] top = el.attrib['layer'] == self.TOP_PLACE_LAYER elif el.attrib['layer'] in [self.TOP_DOCU_LAYER, self.BOT_DOCU_LAYER]: if not populate: return dwg_layer = self.pcbdata['drawings']['fabrication'] top = el.attrib['layer'] == self.TOP_DOCU_LAYER elif el.tag == 'wire' and \ el.attrib['layer'] == self.DIMENSION_LAYER: dwg_layer = self.pcbdata['edges'] top = True else: continue dwg = None if el.tag == 'wire': _dx1 = float(el.attrib['x1']) _dx2 = float(el.attrib['x2']) _dy1 = -float(el.attrib['y1']) _dy2 = -float(el.attrib['y2']) dx1, dy1 = self._rotate(_dx1, _dy1, -angle, mirrored) dx2, dy2 = self._rotate(_dx2, _dy2, -angle, mirrored) x1, y1 = x + dx1, -y + dy1 x2, y2 = x + dx2, -y + dy2 if el.get('curve'): dwg = { 'type': 'arc', 'width': float(el.attrib['width']), 'svgpath': self._curve_to_svgpath(el, x, y, angle, mirrored) } else: dwg = { 'type': 'segment', 'start': [x1, y1], 'end': [x2, y2], 'width': float(el.attrib['width']) } elif el.tag == 'rectangle': _dv = self._rectangle_vertices(el) # Rotate rectangle about component origin based on # component angle dv = [self._rotate(_x, _y, -angle, mirrored) for (_x, _y) in _dv] # Map vertices back to absolute coordinates v = [(x + _x, -y + _y) for (_x, _y) in dv] dwg = { 'type': 'polygon', 'filled': 1, 'pos': [0, 0], 'polygons': [v] } elif el.tag in ['circle', 'hole']: _x = float(el.attrib['x']) _y = -float(el.attrib['y']) dxc, dyc = self._rotate(_x, _y, -angle, mirrored) xc, yc = x + dxc, -y + dyc if el.tag == 'circle': radius = float(el.attrib['radius']) width = float(el.attrib['width']) else: radius = float(el.attrib['drill']) / 2 width = 0 dwg = { 'type': 'circle', 'start': [xc, yc], 'radius': radius, 'width': width } elif el.tag in ['polygonshape', 'polygon']: segs = el if el.tag == 'polygon' \ else el.find('polygonoutlinesegments') dv = self._segments_to_polygon(segs, angle, mirrored) polygon = [[x + v[0], -y + v[1]] for v in dv] dwg = { 'type': 'polygon', 'filled': 1, 'pos': [0, 0], 'polygons': [polygon] } if dwg is not None: if el.tag == 'hole' or \ el.attrib['layer'] == self.DIMENSION_LAYER: dwg_layer.append(dwg) else: bot = not top # Note that in Eagle terminology, 'mirrored' # essentially means 'flipped' (i.e. to the opposite # side of the board) if (mirrored and bot) or (not mirrored and top): dwg_layer['F'].append(dwg) elif (mirrored and top) or (not mirrored and bot): dwg_layer['B'].append(dwg) def _name_to_silk(self, name, x, y, elr, tr, align, size, ratio): angle = tr.angle mirrored = tr.mirrored spin = elr.spin ^ tr.spin if mirrored: angle = -angle if align is None: justify = [-1, 1] elif align == 'center': justify = [0, 0] else: j = align.split('-') alignments = { 'bottom': 1, 'center': 0, 'top': -1, 'left': -1, 'right': 1 } justify = [alignments[ss] for ss in j[::-1]] if (90 < angle <= 270 and not spin) or \ (-90 > angle >= -270 and not spin): angle += 180 justify = [-j for j in justify] dwg = { 'type': 'text', 'text': name, 'pos': [x, y], 'height': size, 'width': size, 'justify': justify, 'thickness': size * ratio, 'attr': [] if not mirrored else ['mirrored'], 'angle': angle } self.font_parser.parse_font_for_string(name) if mirrored: self.pcbdata['drawings']['silkscreen']['B'].append(dwg) else: self.pcbdata['drawings']['silkscreen']['F'].append(dwg) def _element_refdes_to_silk(self, el, package): if 'smashed' not in el.attrib: elx = float(el.attrib['x']) ely = -float(el.attrib['y']) for p_el in package.iter('text'): if p_el.text == '>NAME': dx = float(p_el.attrib['x']) dy = float(p_el.attrib['y']) elr = self.Rot(el.get('rot')) dx, dy = self._rotate(dx, dy, elr.angle, elr.mirrored) tr = self.Rot(p_el.get('rot')) tr.angle += elr.angle tr.mirrored ^= elr.mirrored self._name_to_silk( name=el.attrib['name'], x=elx + dx, y=ely - dy, elr=elr, tr=tr, align=p_el.get('align'), size=float(p_el.attrib['size']), ratio=float(p_el.get('ratio', '8')) / 100) for attr in el.iter('attribute'): if attr.attrib['name'] == 'NAME': self._name_to_silk( name=el.attrib['name'], x=float(attr.attrib['x']), y=-float(attr.attrib['y']), elr=self.Rot(el.get('rot')), tr=self.Rot(attr.get('rot')), align=attr.attrib.get('align'), size=float(attr.attrib['size']), ratio=float(attr.get('ratio', '8')) / 100) @staticmethod def _segments_to_polygon(segs, angle=0, mirrored=False): polygon = [] for vertex in segs.iter('vertex'): _x, _y = float(vertex.attrib['x']), -float(vertex.attrib['y']) x, y = FusionEagleParser._rotate(_x, _y, -angle, mirrored) polygon.append([x, y]) return polygon def _add_zone(self, poly, net): layer = poly.attrib['layer'] if layer == self.TOP_COPPER_LAYER: dest = self.pcbdata['zones']['F'] elif layer == self.BOT_COPPER_LAYER: dest = self.pcbdata['zones']['B'] else: return if poly.tag == 'polygonpour': shapes = poly.find('polygonfilldetails').findall('polygonshape') if shapes: zone = {'polygons': [], 'fillrule': 'evenodd'} for shape in shapes: segs = shape.find('polygonoutlinesegments') zone['polygons'].append(self._segments_to_polygon(segs)) holelist = shape.find('polygonholelist') if holelist: holes = holelist.findall('polygonholesegments') for hole in holes: zone['polygons'].append(self._segments_to_polygon(hole)) if self.config.include_nets: zone['net'] = net dest.append(zone) else: zone = {'polygons': []} zone['polygons'].append(self._segments_to_polygon(poly)) if self.config.include_nets: zone['net'] = net dest.append(zone) def _add_parsed_font_data(self): for (c, wl) in self.font_parser.get_parsed_font().items(): if c not in self.pcbdata['font_data']: self.pcbdata['font_data'][c] = wl def _parse_param_length(self, name, root, default): # parse named parameter (typically a design rule) assuming it is in # length units (mil or mm) p = [el.attrib['value'] for el in root.iter('param') if el.attrib['name'] == name] if len(p) == 0: self.logger.warning("{0} not found, defaulting to {1}" .format(name, default)) return default else: if len(p) > 1: self.logger.warning( "Multiple {0} found, using first occurrence".format(name)) p = p[0] p_val = float(''.join(d for d in p if d in string.digits + '.')) p_units = (''.join(d for d in p if d in string.ascii_lowercase)) if p_units == 'mm': return p_val elif p_units == 'mil': return p_val * 0.0254 else: self.logger.error("Unsupported units {0} on {1}" .format(p_units, name)) def parse(self): ext = os.path.splitext(self.file_name)[1] if ext.lower() == '.fbrd': with zipfile.ZipFile(self.file_name) as myzip: brdfilename = [fname for fname in myzip.namelist() if os.path.splitext(fname)[1] == '.brd'] with myzip.open(brdfilename[0]) as brdfile: return self._parse(brdfile) elif ext.lower() == '.brd': with io.open(self.file_name, 'r', encoding='utf-8') as brdfile: return self._parse(brdfile) def _parse(self, brdfile): try: brdxml = ElementTree.parse(brdfile) except ElementTree.ParseError as err: self.logger.error( "Exception occurred trying to parse {0}, message: {1}" .format(brdfile.name, err.msg)) return None, None if brdxml is None: self.logger.error( "No data was able to be parsed from {0}".format(brdfile.name)) return None, None # Pick out key sections root = brdxml.getroot() board = root.find('drawing').find('board') plain = board.find('plain') elements = board.find('elements') signals = board.find('signals') # Build library mapping elements' pads to nets self._parse_pad_nets(signals) # Parse needed design rules # Minimum via annular ring # (Needed in order to calculate through-hole pad diameters correctly) self.min_via_w = ( self._parse_param_length('rlMinViaOuter', root, default=0)) # Minimum drill diameter above which vias will be un-tented self.min_drill_via_untented = ( self._parse_param_length('mlViaStopLimit', root, default=0)) # Signals --> nets if self.config.include_nets: self.pcbdata['nets'] = [] for signal in signals.iter('signal'): self.pcbdata['nets'].append(signal.attrib['name']) # Signals --> tracks, zones if self.config.include_tracks: self.pcbdata['tracks'] = {'F': [], 'B': []} self.pcbdata['zones'] = {'F': [], 'B': []} for signal in signals.iter('signal'): for wire in signal.iter('wire'): self._add_track(wire, signal.attrib['name']) for via in signal.iter('via'): self._add_track(via, signal.attrib['name']) for poly in signal.iter('polygonpour'): self._add_zone(poly, signal.attrib['name']) # Elements --> components, footprints, silkscreen, edges for el in elements.iter('element'): populate = el.get('populate') != 'no' elr = self.Rot(el.get('rot')) layer = 'B' if elr.mirrored else 'F' extra_fields = {} for a in el.iter('attribute'): if 'value' in a.attrib: extra_fields[a.attrib['name']] = a.attrib['value'] comp = Component(ref=el.attrib['name'], val='' if 'value' not in el.attrib else el.attrib[ 'value'], footprint=el.attrib['package'], layer=layer, attr=None if populate else 'Virtual', extra_fields=extra_fields) # For component, get footprint data libs = [lib for lib in board.find('libraries').findall('library') if lib.attrib['name'] == el.attrib['library']] packages = [] for lib in libs: p = [pac for pac in lib.find('packages').findall('package') if pac.attrib['name'] == el.attrib['package']] packages.extend(p) if not packages: self.logger.error("Package {0} in library {1} not found in " "source file {2} for element {3}" .format(el.attrib['package'], el.attrib['library'], brdfile.name, el.attrib['name'])) return None, None else: package = packages[0] if len(packages) > 1: self.logger.warn("Multiple packages found for package {0}" " in library {1}, using first instance " "found".format(el.attrib['package'], el.attrib['library'])) elx = float(el.attrib['x']) ely = float(el.attrib['y']) refdes = el.attrib['name'] footprint = { 'ref': refdes, 'center': [elx, ely], 'pads': [], 'drawings': [], 'layer': layer } elr = self.Rot(el.get('rot')) footprint['pads'] = self._footprint_pads(package, elx, ely, elr.angle, elr.mirrored, refdes) footprint['bbox'] = self._calculate_footprint_bbox(package, elx, ely, elr.angle, elr.mirrored) self.pcbdata['footprints'].append(footprint) # Add silkscreen, edges for component footprint & refdes self._process_footprint(package, elx, ely, elr.angle, elr.mirrored, populate) self._element_refdes_to_silk(el, package) self.components.append(comp) # Edges & silkscreen (independent of elements) for el in plain.iter(): self._add_drawing(el) # identify board bounding box based on edges board_outline_bbox = BoundingBox() for drawing in self.pcbdata['edges']: self.add_drawing_bounding_box(drawing, board_outline_bbox) if board_outline_bbox.initialized(): self.pcbdata['edges_bbox'] = board_outline_bbox.to_dict() self._add_parsed_font_data() # Fabrication & metadata company = [a.attrib['value'] for a in root.iter('attribute') if a.attrib['name'] == 'COMPANY'] company = '' if not company else company[0] rev = [a.attrib['value'] for a in root.iter('attribute') if a.attrib['name'] == 'REVISION'] rev = '' if not rev else rev[0] if not rev: rev = '' title = os.path.basename(self.file_name) variant = [a.attrib['name'] for a in root.iter('variantdef') if a.get('current') == 'yes'] variant = None if not variant else variant[0] if variant: title = "{0}, Variant: {1}".format(title, variant) date = datetime.fromtimestamp( os.path.getmtime(self.file_name)).strftime('%Y-%m-%d %H:%M:%S') self.pcbdata['metadata'] = {'title': title, 'revision': rev, 'company': company, 'date': date} return self.pcbdata, self.components