#! /usr/bin/env python # -*- coding: utf-8 -*- # copyright 2014 Hamilton Kibbe # Modified from parser.py by Paulo Henrique Silva # # 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. import math import re from .cam import CamFile, FileSettings from .primitives import TestRecord # Net Name Variables _NNAME = re.compile(r'^NNAME\d+$') # Board Edge Coordinates _COORD = re.compile(r'X?(?P[\d\s]*)?Y?(?P[\d\s]*)?') _SM_FIELD = {'0': 'none', '1': 'primary side', '2': 'secondary side', '3': 'both'} def read(filename): """ Read data from filename and return an IPC_D_356 Parameters ---------- filename : string Filename of file to parse Returns ------- file : :class:`gerber.ipc356.IPC_D_356` An IPC_D_356 object created from the specified file. """ # File object should use settings from source file by default. return IPC_D_356.from_file(filename) class IPC_D_356(CamFile): @classmethod def from_file(self, filename): p = IPC_D_356_Parser() return p.parse(filename) 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): return FileSettings(units=self.units, angle_units=self.angle_units) @property def comments(self): return [record for record in self.statements if isinstance(record, IPC356_Comment)] @property def parameters(self): return [record for record in self.statements if isinstance(record, IPC356_Parameter)] @property def test_records(self): return [record for record in self.statements if isinstance(record, IPC356_TestRecord)] @property def nets(self): nets = [] for net in list(set([rec.net_name for rec in self.test_records if rec.net_name is not None])): adjacent_nets = set() for record in self.adjacency_records: if record.net == net: adjacent_nets = adjacent_nets.update(record.adjacent_nets) elif net in record.adjacent_nets: adjacent_nets.add(record.net) nets.append(IPC356_Net(net, adjacent_nets)) return nets @property def components(self): return list(set([rec.id for rec in self.test_records if rec.id is not None and rec.id != 'VIA'])) @property def vias(self): return [rec.id for rec in self.test_records if rec.id == 'VIA'] @property def outlines(self): return [stmt for stmt in self.statements if isinstance(stmt, IPC356_Outline)] @property def adjacency_records(self): return [record for record in self.statements if isinstance(record, IPC356_Adjacency)] 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): self.units = 'inch' self.angle_units = 'degrees' self.statements = [] self.nnames = {} @property def settings(self): return FileSettings(units=self.units, angle_units=self.angle_units) def parse(self, filename): with open(filename, 'rU') as f: oldline = '' for line in f: # 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] == '378': # Conductor self.statements.append(IPC356_Conductor.from_line(line, self.settings)) elif line[0:3] == '379': # Net Adjacency self.statements.append(IPC356_Adjacency.from_line(line)) elif line[0:3] == '389': # Outline self.statements.append(IPC356_Outline.from_line(line, self.settings)) class IPC356_Comment(object): @classmethod def from_line(cls, line): if line[0] != 'C': raise ValueError('Not a valid comment statment') comment = line[2:].strip() return cls(comment) def __init__(self, comment): self.comment = comment def __repr__(self): return '' % self.comment class IPC356_Parameter(object): @classmethod def from_line(cls, line): if line[0] != 'P': raise ValueError('Not a valid parameter statment') splitline = line[2:].split() parameter = splitline[0].strip() value = ' '.join(splitline[1:]).strip() return cls(parameter, value) def __init__(self, parameter, value): self.parameter = parameter self.value = value def __repr__(self): return '' % (self.parameter, self.value) class IPC356_TestRecord(object): @classmethod def from_line(cls, line, settings): offset = 0 units = settings.units angle = settings.angle_units feature_types = {'1':'through-hole', '2': 'smt', '3':'tooling-feature', '4':'tooling-hole'} access = ['both', 'top', 'layer2', 'layer3', 'layer4', 'layer5', 'layer6', 'layer7', 'bottom'] record = {} line = line.strip() if line[0] != '3': raise ValueError('Not a valid test record statment') record['feature_type'] = feature_types[line[1]] end = len(line) - 1 if len(line) < 18 else 17 record['net_name'] = line[3:end].strip() if len(line) >= 27 and line[26] != '-': offset = line[26:].find('-') offset = 0 if offset == -1 else offset end = len(line) - 1 if len(line) < (27 + offset) else (26 + offset) record['id'] = line[20:end].strip() end = len(line) - 1 if len(line) < (32 + offset) else (31 + offset) record['pin'] = (line[27 + offset:end].strip() if line[27 + offset:end].strip() != '' else None) record['location'] = 'middle' if line[31 + offset] == 'M' else 'end' if line[32 + offset] == 'D': end = len(line) - 1 if len(line) < (38 + offset) else (37 + offset) dia = int(line[33 + offset:end].strip()) record['hole_diameter'] = (dia * 0.0001 if units == 'inch' else dia * 0.001) if len(line) >= (38 + offset): record['plated'] = (line[37 + offset] == 'P') if len(line) >= (40 + offset): end = len(line) - 1 if len(line) < (42 + offset) else (41 + offset) record['access'] = access[int(line[39 + offset:end])] if len(line) >= (43 + offset): end = len(line) - 1 if len(line) < (50 + offset) else (49 + offset) coord = int(line[42 + offset:end].strip()) record['x_coord'] = (coord * 0.0001 if units == 'inch' else coord * 0.001) if len(line) >= (51 + offset): end = len(line) - 1 if len(line) < (58 + offset) else (57 + offset) coord = int(line[50 + offset:end].strip()) record['y_coord'] = (coord * 0.0001 if units == 'inch' else coord * 0.001) if len(line) >= (59 + offset): end = len(line) - 1 if len(line) < (63 + offset) else (62 + offset) dim = line[58 + offset:end].strip() if dim != '': record['rect_x'] = (int(dim) * 0.0001 if units == 'inch' else int(dim) * 0.001) if len(line) >= (64 + offset): end = len(line) - 1 if len(line) < (68 + offset) else (67 + offset) dim = line[63 + offset:end].strip() if dim != '': record['rect_y'] = (int(dim) * 0.0001 if units == 'inch' else int(dim) * 0.001) if len(line) >= (69 + offset): end = len(line) - 1 if len(line) < (72 + offset) else (71 + offset) rot = line[68 + offset:end].strip() if rot != '': record['rect_rotation'] = (int(rot) if angle == 'degrees' else math.degrees(rot)) if len(line) >= (74 + offset): end = 74 + offset sm_info = line[73 + offset:end].strip() record['soldermask_info'] = _SM_FIELD.get(sm_info) if len(line) >= (76 + offset): end = len(line) - 1 if len(line) < (80 + offset) else 79 + offset record['optional_info'] = line[75 + offset:end] return cls(**record) def __init__(self, **kwargs): for key in kwargs: setattr(self, key, kwargs[key]) def __repr__(self): return '' % (self.net_name, self.feature_type) class IPC356_Outline(object): @classmethod def from_line(cls, line, settings): type = line[3:17].strip() scale = 0.0001 if settings.units == 'inch' else 0.001 points = [] x = 0 y = 0 coord_strings = line.strip().split()[1:] for coord in coord_strings: coord_dict = _COORD.match(coord).groupdict() x = int(coord_dict['x']) if coord_dict['x'] is not '' else x y = int(coord_dict['y']) if coord_dict['y'] is not '' else y points.append((x * scale, y * scale)) return cls(type, points) def __init__(self, type, points): self.type = type self.points = points def __repr__(self): return '' % self.type class IPC356_Conductor(object): @classmethod def from_line(cls, line, settings): if line[0:3] != '378': raise ValueError('Not a valid IPC-D-356 Conductor statement') scale = 0.0001 if settings.units == 'inch' else 0.001 net_name = line[3:17].strip() layer = int(line[19:21]) # Parse out aperture definiting raw_aperture = line[22:].split()[0] aperture_dict = _COORD.match(raw_aperture).groupdict() x = 0 y = 0 x = int(aperture_dict['x']) * scale if aperture_dict['x'] is not '' else None y = int(aperture_dict['y']) * scale if aperture_dict['y'] is not '' else None aperture = (x, y) # Parse out conductor shapes shapes = [] coord_list = ' '.join(line[22:].split()[1:]) raw_shapes = coord_list.split('*') for rshape in raw_shapes: x = 0 y = 0 shape = [] coords = rshape.split() for coord in coords: coord_dict = _COORD.match(coord).groupdict() x = int(coord_dict['x']) if coord_dict['x'] is not '' else x y = int(coord_dict['y']) if coord_dict['y'] is not '' else y shape.append((x * scale, y * scale)) shapes.append(tuple(shape)) return cls(net_name, layer, aperture, tuple(shapes)) def __init__(self, net_name, layer, aperture, shapes): self.net_name = net_name self.layer = layer self.aperture = aperture self.shapes = shapes def __repr__(self): return '' % self.net_name class IPC356_Adjacency(object): @classmethod def from_line(cls, line): if line[0:3] != '379': raise ValueError('Not a valid IPC-D-356 Conductor statement') nets = line[3:].strip().split() return cls(nets[0], nets[1:]) def __init__(self, net, adjacent_nets): self.net = net self.adjacent_nets = adjacent_nets def __repr__(self): return '' % self.net class IPC356_EndOfFile(object): def __init__(self): pass def to_netlist(self): return '999' def __repr__(self): return '' class IPC356_Net(object): def __init__(self, name, adjacent_nets): self.name = name self.adjacent_nets = set(adjacent_nets) if adjacent_nets is not None else set() def __repr__(self): return '' % self.name