def __init__(self): self.design = Design() # map (component, gate name) to body indices self.cptgate2body_index = {} # map (component, gate name) to pin maps, dicts from strings # (pin names) to Pins. These are used during pinref processing # in segments. self.cptgate2pin_map = defaultdict(dict) # map (component, gate names) to annotation maps, dicts from # strings (name|value) to Annotations. These represent the # >NAME and >VALUE texts on eagle components, which must be # converted into component instance annotations since their # contents depend on the component instance name and value. self.cptgate2ann_map = defaultdict(dict) # map part names to component instances. These are used during # pinref processing in segments. self.part2inst = {} # map part names to gate names to symbol attributes. These # are used during pinref processing in segments. self.part2gate2symattr = defaultdict(dict)
def parse(self, filename, library_filename=None): """ Parse a kicad file into a design """ design = Design() segments = set() # each wire segment junctions = set() # wire junction point (connects all wires under it) self.instance_names = [] self.library = KiCADLibrary() if library_filename is None: directory, _ = split(filename) for dir_file in listdir(directory): if dir_file.endswith('.lib'): self.library.parse(directory + '/' + dir_file) for cpt in self.library.components: design.add_component(cpt.name, cpt) with open(filename) as f: libs = [] line = f.readline().strip() # parse the library references while line and line != "$EndDescr": if line.startswith('LIBS:'): libs.extend(line.split(':', 1)[1].split(',')) line = f.readline().strip() # Now parse wires and components, ignore connections, we get # connectivity from wire segments line = f.readline() while line: prefix = line.split()[0] if line.startswith('Wire Wire Line'): self.parse_wire(f, segments) elif prefix == "Connection": # Store these to apply later self.parse_connection(line, junctions) elif prefix == "Text": design.design_attributes.add_annotation( self.parse_text(f, line)) elif prefix == "$Comp": # Component Instance inst, comp = self.parse_component_instance(f) design.add_component_instance(inst) if comp is not None: design.add_component(comp.name, comp) self.ensure_component(design, inst.library_id, libs) line = f.readline() segments = self.divide(segments, junctions) design.nets = self.calc_nets(design, segments) design.scale(MULT) return design
def test_units(self): """ Capture absence of units. """ layout = Layout() layout.units = None layout.layers.append(Layer()) design = Design() design.layout = layout writer = Writer() writer.write(design)
def parse(self): '''Returns a Design built up from a schematic file that represents one sheet of the original schematic''' tree = ViewDrawBase.parse(self) # tree['lines'] is a [list of [list of lines]] tree['shape'].extend(sum(tree['lines'], [])) ckt = Design() # TODO little weak here, a copy instead? ckt.components = self.lib for net in tree['net']: ckt.add_net(net) for inst in tree['inst']: ckt.add_component_instance(inst) # hold on tight, this is ugly for (netid, netpt, pinid) in inst.conns: net = [n for n in ckt.nets if n.net_id == netid][0] comp = ConnectedComponent(inst.instance_id, pinid) net.ibpts[netpt - 1].add_connected_component(comp) del inst.conns for net in ckt.nets: del net.ibpts for shape in tree['shape']: ckt.add_shape(shape) if isinstance(shape, Label): ann = Annotation(shape.text, shape.x, shape.y, shape._rotation, True) ckt.design_attributes.add_annotation(ann) for k, v, annot in tree['attr']: ckt.design_attributes.add_attribute(k, v) ckt.design_attributes.add_annotation(annot) return ckt
def test_images(self): """ Capture images with no data. """ layer = Layer() layer.images.append(Image()) layout = Layout() layout.units = 'mm' layout.layers.append(layer) design = Design() design.layout = layout writer = Writer() writer.write(design)
def parse(self, infile='.'): """ Parse tokens from gerber files into a design. """ is_zip = infile.endswith('.zip') openarchive = ZipFile if is_zip else TarFile.open archive = batch_member = None try: # define multiple layers from folder if LAYERS_CFG in infile: archive = None cfg_name = infile cfg = open(cfg_name, 'r') # define multiple layers from archivea else: archive = openarchive(infile) batch = archive.namelist if is_zip else archive.getnames batch_member = archive.open if is_zip else archive.extractfile cfg_name = [n for n in batch() if LAYERS_CFG in n][0] cfg = batch_member(cfg_name) # define single layer from single gerber file except ReadError: name, ext = path.split(infile)[1].rsplit('.', 1) layer_defs = [ LayerDef(ext.lower() == 'ger' and name or ext, 'unknown', infile) ] self._gen_layers(layer_defs, None, None) # tidy up batch specs else: layer_defs = [ LayerDef(rec[0], rec[1], path.join(path.split(cfg_name)[0], rec[2])) for rec in csv.reader(cfg, skipinitialspace=True) ] cfg.close() self._gen_layers(layer_defs, archive, batch_member) # tidy up archive finally: if archive: archive.close() # compile design if DEBUG: self._debug_stdout() self.layout.units = (self.params['MO'] == 'IN' and 'inch' or 'mm') design = Design() design.layout = self.layout return design
def test_generating_geda_commands_for_toplevel_shapes(self): design = Design() design.shapes = [ shape.Line((0, 0), (0, 50)), shape.Circle(0, 0, 300), ] design.pins = [ components.Pin('E', (0, 0), (0, 30)), components.Pin('E', (0, 0), (0, 30)), ] commands = self.geda_writer.generate_body_commands(design) ## default pins require 6 commands, shapes require 1 command self.assertEquals(len(commands), 2 * 6 + 2 * 1)
def test_generating_geda_commands_for_toplevel_shapes(self): design = Design() design.shapes = [ shape.Line((0, 0), (0, 50)), shape.Circle(0, 0, 300), ] design.pins = [ components.Pin('E', (0, 0), (0, 30)), components.Pin('E', (0, 0), (0, 30)), ] commands = self.geda_writer.generate_body_commands(design) ## default pins require 6 commands, shapes require 1 command self.assertEquals(len(commands), 2*6 + 2*1)
def parse(self, infile='.'): """ Parse tokens from gerber files into a design. """ is_zip = infile.endswith('.zip') openarchive = ZipFile if is_zip else TarFile.open archive = batch_member = None try: # define multiple layers from folder if LAYERS_CFG in infile: archive = None cfg_name = infile cfg = open(cfg_name, 'r') # define multiple layers from archivea else: archive = openarchive(infile) batch = archive.namelist if is_zip else archive.getnames batch_member = archive.open if is_zip else archive.extractfile cfg_name = [n for n in batch() if LAYERS_CFG in n][0] cfg = batch_member(cfg_name) # define single layer from single gerber file except ReadError: name, ext = path.split(infile)[1].rsplit('.', 1) layer_defs = [LayerDef(ext.lower() == 'ger' and name or ext, 'unknown', infile)] self._gen_layers(layer_defs, None, None) # tidy up batch specs else: layer_defs = [LayerDef(rec[0], rec[1], path.join(path.split(cfg_name)[0], rec[2])) for rec in csv.reader(cfg, skipinitialspace=True)] cfg.close() self._gen_layers(layer_defs, archive, batch_member) # tidy up archive finally: if archive: archive.close() # compile design if DEBUG: self._debug_stdout() self.layout.units = (self.params['MO'] == 'IN' and 'inch' or 'mm') design = Design() design.layout = self.layout return design
def parse(self, filename): """ Parse a specctra file into a design """ self.design = Design() with open(filename) as f: data = f.read() tree = DsnParser().parse(data) struct = self.walk(tree) self.resolution = struct.resolution self._convert(struct) return self.design
def parse(self, inputfile): """ Parse a gEDA file into a design. Returns the design corresponding to the gEDA file. """ inputfiles = [] ## check if inputfile is in ZIP format if zipfile.is_zipfile(inputfile): self.geda_zip = zipfile.ZipFile(inputfile) for filename in self.geda_zip.namelist(): if filename.endswith('.sch'): inputfiles.append(filename) else: inputfiles = [inputfile] self.design = Design() ## parse frame data of first schematic to extract ## page size (assumes same frame for all files) with self._open_file_or_zip(inputfiles[0]) as stream: self._check_version(stream) for line in stream.readlines(): if 'title' in line and line.startswith('C'): obj_type, params = self._parse_command(StringIO(line)) assert(obj_type == 'C') params['basename'], _ = os.path.splitext( params['basename'], ) log.debug("using title file: %s", params['basename']) self._parse_title_frame(params) ## store offset values in design attributes self.design.design_attributes.attributes.update({ '_geda_offset_x': str(self.offset.x), '_geda_offset_y': str(self.offset.y), '_geda_frame_width': str(self.frame_width), '_geda_frame_height': str(self.frame_height), }) for filename in inputfiles: f_in = self._open_file_or_zip(filename) self._check_version(f_in) self.parse_schematic(f_in) basename, _ = os.path.splitext(os.path.basename(filename)) self.design.design_attributes.metadata.set_name(basename) ## modify offset for next page to be shifted to the right self.offset.x = self.offset.x - self.frame_width f_in.close() return self.design
def __init__(self): self.design = Design() # This maps fritzing connector keys to (x, y) coordinates self.points = {} # (index, connid) -> (x, y) # This maps fritzing component indices to ComponentInstances self.component_instances = {} # index -> ComponentInstance # Map connector keys to the list of connector keys they # are connected to. self.connects = {} # (index, connid) -> [(index, connid)] self.components = {} # idref -> ComponentParser self.fritzing_version = None self.fzz_zipfile = None # The ZipFile if we are parsing an fzz
def test_write_header(self): """ The write_header method produces the right string. """ design = Design() design.design_attributes.metadata.updated_timestamp = 0 writer = KiCAD() buf = StringIO() writer.write_header(buf, design) self.assertEqual(buf.getvalue()[:40], 'EESchema Schematic File Version 2 date ')
def parse(self): '''Returns a Design built up from a schematic file that represents one sheet of the original schematic''' tree = ViewDrawBase.parse(self) # tree['lines'] is a [list of [list of lines]] tree['shape'].extend(sum(tree['lines'], [])) ckt = Design() # TODO little weak here, a copy instead? ckt.components = self.lib for net in tree['net']: ckt.add_net(net) for inst in tree['inst']: ckt.add_component_instance(inst) # hold on tight, this is ugly for (netid, netpt, pinid) in inst.conns: net = [n for n in ckt.nets if n.net_id == netid][0] comp = ConnectedComponent(inst.instance_id, pinid) net.ibpts[netpt - 1].add_connected_component(comp) del inst.conns for net in ckt.nets: del net.ibpts # too bad designs don't have top-level shapes (yet?) #map(ckt.add_shape, tree['shape']) for lbl in [s for s in tree['shapes'] if isinstance(s, Label)]: ann = Annotation(lbl.text, lbl.x, lbl.y, lbl.rotation, True) ckt.design_attributes.add_annotation(ann) for k, v in tree['attr']: ckt.design_attributes.add_attribute(k, v) self.correct_y(ckt, tree['Dbounds'][0]) return ckt
def parse(self, filename, library_filename=None): """ Parse a kicad file into a design """ design = Design() segments = set() # each wire segment junctions = set() # wire junction point (connects all wires under it) if library_filename is None: library_filename = splitext(filename)[0] + '-cache.lib' if exists(library_filename): for cpt in parse_library(library_filename): design.add_component(cpt.name, cpt) with open(filename) as f: libs = [] line = f.readline().strip() # parse the library references while line and line != "$EndDescr": if line.startswith('LIBS:'): libs.extend(line.split(':', 1)[1].split(',')) line = f.readline().strip() # Now parse wires and components, ignore connections, we get # connectivity from wire segments line = f.readline() while line: prefix = line.split()[0] if line.startswith('Wire Wire Line'): self.parse_wire(f, segments) elif prefix == "Connection": # Store these to apply later self.parse_connection(line, junctions) elif prefix == "Text": design.design_attributes.add_annotation( self.parse_text(f, line)) elif prefix == "$Comp": # Component Instance inst = self.parse_component_instance(f) design.add_component_instance(inst) if inst.library_id not in design.components.components: cpt = lookup_part(inst.library_id, libs) if cpt is not None: design.components.add_component(cpt.name, cpt) line = f.readline() segments = self.divide(segments, junctions) design.nets = self.calc_nets(segments) self.calc_connected_components(design) return design
def parse_schematic(self, stream): """ Parse a gEDA schematic provided as a *stream* object into a design. Returns the design corresponding to the schematic. """ # pylint: disable=R0912 if self.design is None: self.design = Design() self.segments = set() self.net_points = dict() self.net_names = dict() obj_type, params = self._parse_command(stream) while obj_type is not None: objects = getattr(self, "_parse_%s" % obj_type)(stream, params) attributes = self._parse_environment(stream) self.design.design_attributes.attributes.update(attributes or {}) self.add_objects_to_design(self.design, objects) obj_type, params = self._parse_command(stream) ## process net segments into nets & net points and add to design self.divide_segments() calculated_nets = self.calculate_nets() for cnet in sorted(calculated_nets, key=lambda n: n.net_id): self.design.add_net(cnet) return self.design
def parse_schematic(self, stream): """ Parse a gEDA schematic provided as a *stream* object into a design. Returns the design corresponding to the schematic. """ # pylint: disable=R0912 if self.design is None: self.design = Design() self.segments = set() self.net_points = dict() self.net_names = dict() obj_type, params = self._parse_command(stream) while obj_type is not None: objects = getattr(self, "_parse_%s" % obj_type)(stream, params) attributes = self._parse_environment(stream) self.design.design_attributes.attributes.update(attributes or {}) self.add_objects_to_design(self.design, objects) obj_type, params = self._parse_command(stream) ## process net segments into nets & net points and add to design self.divide_segments() calculated_nets = self.calculate_nets() for cnet in sorted(calculated_nets, key=lambda n : n.net_id): self.design.add_net(cnet) return self.design
class EagleXML(object): """ The Eagle XML Format Parser. This parser uses code generated by generateDS.py which converts an xsd file to a set of python objects with parse and export functions. That code is in generated.py. It was created by the following steps: 1. Started with eagle.dtd from Eagle 6.2.0. 2. Removed inline comments in dtd (was breaking conversion to xsd). The dtd is also stored in this directory. 3. Converted to eagle.xsd using dtd2xsd.pl from w3c. The xsd is also stored in this directory. 4. Run a modified version of generateDS.py with the following arguments: --silence --external-encoding=utf-8 -o generated.py """ SCALE = 2.0 MULT = 90 / 25.4 # mm to 90 dpi def __init__(self): self.design = Design() # map (component, gate name) to body indices self.cptgate2body_index = {} # map (component, gate name) to pin maps, dicts from strings # (pin names) to Pins. These are used during pinref processing # in segments. self.cptgate2pin_map = defaultdict(dict) # map (component, gate names) to annotation maps, dicts from # strings (name|value) to Annotations. These represent the # >NAME and >VALUE texts on eagle components, which must be # converted into component instance annotations since their # contents depend on the component instance name and value. self.cptgate2ann_map = defaultdict(dict) # map part names to component instances. These are used during # pinref processing in segments. self.part2inst = {} # map part names to gate names to symbol attributes. These # are used during pinref processing in segments. self.part2gate2symattr = defaultdict(dict) @staticmethod def auto_detect(filename): """ Return our confidence that the given file is an eagle xml schematic """ with open(filename, 'r') as f: data = f.read(4096) confidence = 0.0 if 'eagle.dtd' in data: confidence += 0.9 return confidence def parse(self, filename): """ Parse an Eagle XML file into a design """ root = parse(filename) self.make_components(root) self.make_component_instances(root) self.make_nets(root) self.design.scale(EAGLE_SCALE) return self.design def make_components(self, root): """ Construct openjson components from an eagle model. """ for lib in get_subattr(root, 'drawing.schematic.libraries.library', ()): for deviceset in get_subattr(lib, 'devicesets.deviceset', ()): for cpt in self.make_deviceset_components(lib, deviceset): self.design.components.add_component(cpt.name, cpt) def make_deviceset_components(self, lib, deviceset): """ Construct openjson components for each device in an eaglexml deviceset in a library.""" for device in deviceset.devices.device: yield self.make_device_component(lib, deviceset, device) def make_device_component(self, lib, deviceset, device): """ Construct an openjson component for a device in a deviceset. """ cpt = Component(lib.name + ':' + deviceset.name + ':' + device.name) cpt.add_attribute('eaglexml_library', lib.name) cpt.add_attribute('eaglexml_deviceset', deviceset.name) cpt.add_attribute('eaglexml_device', device.name) symbol = Symbol() cpt.add_symbol(symbol) assignment = PinNumberAssignment(device) for i, gate in enumerate(get_subattr(deviceset, 'gates.gate')): body, pin_map, ann_map = self.make_body_from_symbol( lib, gate.symbol, assignment.get_pin_number_lookup(gate.name)) symbol.add_body(body) cpt.add_attribute('eaglexml_symbol_%d' % i, gate.symbol) cpt.add_attribute('eaglexml_gate_%d' % i, gate.name) self.cptgate2body_index[cpt, gate.name] = len(symbol.bodies) - 1 self.cptgate2pin_map[cpt, gate.name] = pin_map self.cptgate2ann_map[cpt, gate.name] = ann_map return cpt def make_body_from_symbol(self, lib, symbol_name, pin_number_lookup): """ Construct an openjson SBody from an eagle symbol in a library. """ body = SBody() symbol = [ s for s in get_subattr(lib, 'symbols.symbol') if s.name == symbol_name ][0] for wire in symbol.wire: body.add_shape(self.make_shape_for_wire(wire)) for rect in symbol.rectangle: rotation = make_angle('0' if rect.rot is None else rect.rot) x1, y1 = rotate_point( (self.make_length(rect.x1), self.make_length(rect.y1)), rotation) x2, y2 = rotate_point( (self.make_length(rect.x2), self.make_length(rect.y2)), rotation) ux, uy = min(x1, x2), max(y1, y2) lx, ly = max(x1, x2), min(y1, y2) body.add_shape(Rectangle(ux, uy, lx - ux, uy - ly)) for poly in symbol.polygon: map(body.add_shape, self.make_shapes_for_poly(poly)) for circ in symbol.circle: body.add_shape(self.make_shape_for_circle(circ)) pin_map = {} for pin in symbol.pin: connect_point = (self.make_length(pin.x), self.make_length(pin.y)) null_point = self.get_pin_null_point(connect_point, pin.length, pin.rot) label = self.get_pin_label(pin, null_point) pin_map[pin.name] = Pin(pin_number_lookup(pin.name), null_point, connect_point, label) if pin.direction: pin_map[pin.name].add_attribute('eaglexml_direction', pin.direction) if pin.visible: pin_map[pin.name].add_attribute('eaglexml_visible', pin.visible) body.add_pin(pin_map[pin.name]) ann_map = {} for text in symbol.text: x = self.make_length(text.x) y = self.make_length(text.y) content = '' if text.valueOf_ is None else text.valueOf_ rotation = make_angle('0' if text.rot is None else text.rot) align = 'right' if is_mirrored(text.rot) else 'left' if rotation == 0.5: rotation = 1.5 if content.lower() == '>name': ann_map['name'] = Annotation(content, x, y, rotation, 'true') elif content.lower() == '>value': ann_map['value'] = Annotation(content, x, y, rotation, 'true') else: body.add_shape( Label(x, y, content, align=align, rotation=rotation)) return body, pin_map, ann_map def make_shape_for_wire(self, wire): """ Generate an openjson shape for an eaglexml wire. """ if wire.curve is None: return Line((self.make_length(wire.x1), self.make_length(wire.y1)), (self.make_length(wire.x2), self.make_length(wire.y2))) curve, x1, y1, x2, y2 = map( float, (wire.curve, wire.x1, wire.y1, wire.x2, wire.y2)) if curve < 0: curve = -curve negative = True mult = -1.0 else: negative = False mult = 1.0 if curve > 180.0: major_arc = True curve = 360.0 - curve mult *= -1.0 else: major_arc = False chordlen = sqrt(pow(x2 - x1, 2) + pow(y2 - y1, 2)) radius = chordlen / (2.0 * sin(radians(curve) / 2)) mx, my = (x1 + x2) / 2, (y1 + y2) / 2 # midpoint between arc points h = sqrt(pow(radius, 2) - pow(chordlen / 2, 2)) # height of isoceles # calculate center point cx = mx + mult * h * (y1 - y2) / chordlen cy = my + mult * h * (x2 - x1) / chordlen if negative: start_angle = atan2(y2 - cy, x2 - cx) end_angle = start_angle + radians(curve) - (pi if major_arc else 0.0) else: start_angle = atan2(y1 - cy, x1 - cx) end_angle = start_angle + radians(curve) + (pi if major_arc else 0.0) return Arc(self.make_length(cx), self.make_length(cy), round(start_angle / pi, 3) % 2.0, round(end_angle / pi, 3) % 2.0, self.make_length(radius)) def make_shapes_for_poly(self, poly): """ Generate openjson shapes for an eaglexml polygon. """ # TODO: handle curves opoly = Polygon() for vertex in poly.vertex: opoly.add_point(self.make_length(vertex.x), self.make_length(vertex.y)) yield opoly def make_shape_for_circle(self, circ): """ Generate an openjson shape for an eaglexml circle. """ ocirc = Circle(self.make_length(circ.x), self.make_length(circ.y), self.make_length(circ.radius)) ocirc.add_attribute('eaglexml_width', circ.width) return ocirc def get_pin_null_point(self, (x, y), length, rotation): """ Return the null point of a pin given its connect point, length, and rotation. """ if length == 'long': distance = int(27 * self.SCALE) # .3 inches elif length == 'middle': distance = int(18 * self.SCALE) # .2 inches elif length == 'short': distance = int(9 * self.SCALE) # .1 inches else: # point distance = 0 if rotation is None: rotation = "" if rotation.endswith('R90'): coords = (x, y + distance) elif rotation.endswith('R180'): coords = (x - distance, y) elif rotation.endswith('R270'): coords = (x, y - distance) else: coords = (x + distance, y) if is_mirrored(rotation): x, y = coords coords = (-x, y) return coords
class DesignTests(unittest.TestCase): """ The tests of the core module design feature """ def setUp(self): """ Setup the test case. """ self.des = Design() def tearDown(self): """ Teardown the test case. """ pass def test_create_new_design(self): """ Test the creation of a new empty design. """ self.assertEqual(len(self.des.nets), 0) def test_empty_bounds(self): '''bounds() on an empty design is to include just the origin''' for point in self.des.bounds(): self.assertEqual(point.x, 0) self.assertEqual(point.y, 0) def test_bounds_nets(self): '''Test bounds() with just the design's nets''' leftnet = Net('foo1') topnet = Net('foo2') rightnet = Net('foo3') botnet = Net('foo4') # limits minx=2, miny=1, maxx=7, maxy=9 mkbounds(leftnet, 2, 3, 3, 3) mkbounds(topnet, 3, 1, 3, 3) mkbounds(rightnet, 3, 3, 7, 3) mkbounds(botnet, 3, 3, 3, 9) self.des.add_net(topnet) self.des.add_net(rightnet) self.des.add_net(leftnet) self.des.add_net(botnet) top_left, btm_right = self.des.bounds() self.assertEqual(top_left.x, 2) self.assertEqual(top_left.y, 1) self.assertEqual(btm_right.x, 7) self.assertEqual(btm_right.y, 9) def test_bounds_annots(self): '''Test bounds() with just Annotations added as design attributes''' left = Annotation('foo1', 3, 3, 0, True) top = Annotation('foo2', 3, 3, 0, True) right = Annotation('foo3', 3, 3, 0, True) bot = Annotation('foo4', 3, 3, 0, True) mkbounds(left, 2, 3, 3, 3) mkbounds(top, 3, 2, 3, 3) mkbounds(right, 3, 3, 5, 3) mkbounds(bot, 3, 3, 3, 6) for anno in (left, right, bot, top): self.des.design_attributes.add_annotation(anno) top_left, btm_right = self.des.bounds() self.assertEqual(top_left.x, 2) self.assertEqual(top_left.y, 2) self.assertEqual(btm_right.x, 5) self.assertEqual(btm_right.y, 6) def test_bounds_parts(self): '''test bounds() with just components in the design''' libcomp = Component('bar') libcomp.add_symbol(Symbol()) libcomp.symbols[0].add_body(Body()) mkbounds(libcomp.symbols[0].bodies[0], 0, 0, 10, 10) self.des.add_component('foo', libcomp) for (x, y) in ((1, 3), (3, 2), (5, 3), (3, 7)): compinst = ComponentInstance(str((x, y)), 'foo', 0) compinst.add_symbol_attribute(SymbolAttribute(x, y, 0, False)) self.des.add_component_instance(compinst) top_left, btm_right = self.des.bounds() self.assertEqual(top_left.x, 1) self.assertEqual(top_left.y, 2) self.assertEqual(btm_right.x, 15) self.assertEqual(btm_right.y, 17) def test_bounds_neg_coords(self): '''Test bounds() when the schematic is all negative coordinates''' net = Net('foo') mkbounds(net, -1, -2, -3, -4) self.des.add_net(net) top_left, btm_right = self.des.bounds() self.assertEqual(top_left.x, -3) self.assertEqual(top_left.y, -4) self.assertEqual(btm_right.x, -1) self.assertEqual(btm_right.y, -2) def test_bounds_all_elts(self): '''bounds() with all the elements competing''' net = Net('foo') mkbounds(net, 3, 3, -1, -2) self.des.add_net(net) annot = Annotation('foo', 3, 3, 0, True) mkbounds(annot, 3, 3, 3, 5) self.des.design_attributes.add_annotation(annot) libcomp = Component('bar') libcomp.add_symbol(Symbol()) libcomp.symbols[0].add_body(Body()) mkbounds(libcomp.symbols[0].bodies[0], 0, 0, 3, 3) self.des.add_component('foo', libcomp) compinst = ComponentInstance('bar', 'foo', 0) compinst.add_symbol_attribute(SymbolAttribute(3, 0, 0, False)) self.des.add_component_instance(compinst) top_left, btm_right = self.des.bounds() self.assertEqual(top_left.x, -1) self.assertEqual(top_left.y, -2) self.assertEqual(btm_right.x, 6) self.assertEqual(btm_right.y, 5)
class GEDA: """ The GEDA Format Parser """ DELIMITER = ' ' SCALE_FACTOR = 10.0 # maps 1000 MILS to 10 pixels OBJECT_TYPES = { 'v': geda_commands.GEDAVersionCommand(), 'L': geda_commands.GEDALineCommand(), 'B': geda_commands.GEDABoxCommand(), 'V': geda_commands.GEDACircleCommand(), 'A': geda_commands.GEDAArcCommand(), 'T': geda_commands.GEDATextCommand(), 'N': geda_commands.GEDASegmentCommand(), 'U': geda_commands.GEDABusCommand(), 'P': geda_commands.GEDAPinCommand(), 'C': geda_commands.GEDAComponentCommand(), 'H': geda_commands.GEDAPathCommand(), ## valid types but are ignored 'G': geda_commands.GEDAPictureCommand(), ## environments '{': geda_commands.GEDAEmbeddedEnvironmentCommand(), '}': [], # attributes '[': geda_commands.GEDAAttributeEnvironmentCommand(), ']': [], # embedded component } def __init__(self, symbol_dirs=None): """ Constuct a gEDA parser object. Specifying a list of symbol directories in *symbol_dir* will provide a symbol file lookup in the specified directories. The lookup will be generated instantly examining each directory (if it exists). Kwargs: symbol_dirs (list): List of directories containing .sym files """ self.offset = shape.Point(40000, 40000) ## Initialise frame size with largest possible size self.frame_width = 0 self.frame_height = 0 # initialise PIN counter self.pin_counter = itertools.count(0) # initialise PATH counter self.path_counter = itertools.count(0) ## add flag to allow for auto inclusion if symbol_dirs is None: symbol_dirs = [] symbol_dirs = symbol_dirs + \ [os.path.join(os.path.dirname(__file__), '..', 'library', 'geda')] self.known_symbols = find_symbols(symbol_dirs) self.design = None self.segments = None self.net_points = None self.net_names = None self.geda_zip = None @staticmethod def auto_detect(filename): """ Return our confidence that the given file is an geda schematic """ with open(filename, 'rU') as f: data = f.read() confidence = 0 if data[0:2] == 'v ': confidence += 0.51 if 'package=' in data: confidence += 0.25 if 'footprint=' in data: confidence += 0.25 if 'refdes=' in data: confidence += 0.25 if 'netname=' in data: confidence += 0.25 return confidence def set_offset(self, point): """ Set the offset point for the gEDA output. As OpenJSON positions the origin in the center of the viewport and gEDA usually uses (40'000, 40'000) as page origin, this allows for translating from one coordinate system to another. It expects a *point* object providing a *x* and *y* attribute. """ ## create an offset of 5 grid squares from origin (0,0) self.offset.x = point.x self.offset.y = point.y def parse(self, inputfile): """ Parse a gEDA file into a design. Returns the design corresponding to the gEDA file. """ inputfiles = [] ## check if inputfile is in ZIP format if zipfile.is_zipfile(inputfile): self.geda_zip = zipfile.ZipFile(inputfile) for filename in self.geda_zip.namelist(): if filename.endswith('.sch'): inputfiles.append(filename) else: inputfiles = [inputfile] self.design = Design() ## parse frame data of first schematic to extract ## page size (assumes same frame for all files) with self._open_file_or_zip(inputfiles[0]) as stream: self._check_version(stream) for line in stream.readlines(): if 'title' in line and line.startswith('C'): obj_type, params = self._parse_command(StringIO(line)) assert(obj_type == 'C') params['basename'], _ = os.path.splitext( params['basename'], ) log.debug("using title file: %s", params['basename']) self._parse_title_frame(params) ## store offset values in design attributes self.design.design_attributes.attributes.update({ '_geda_offset_x': str(self.offset.x), '_geda_offset_y': str(self.offset.y), '_geda_frame_width': str(self.frame_width), '_geda_frame_height': str(self.frame_height), }) for filename in inputfiles: f_in = self._open_file_or_zip(filename) self._check_version(f_in) self.parse_schematic(f_in) basename, _ = os.path.splitext(os.path.basename(filename)) self.design.design_attributes.metadata.set_name(basename) ## modify offset for next page to be shifted to the right self.offset.x = self.offset.x - self.frame_width f_in.close() return self.design def _parse_v(self, stream, params): """ Only required to be callable when 'v' command is found. Returns without any processing. """ return def _parse_G(self, stream, params): """ Parse picture command 'G'. Returns without any processing but logs a warning. """ log.warn("ignoring picture/font in gEDA file. Not supported!") return def parse_schematic(self, stream): """ Parse a gEDA schematic provided as a *stream* object into a design. Returns the design corresponding to the schematic. """ # pylint: disable=R0912 if self.design is None: self.design = Design() self.segments = set() self.net_points = dict() self.net_names = dict() obj_type, params = self._parse_command(stream) while obj_type is not None: objects = getattr(self, "_parse_%s" % obj_type)(stream, params) attributes = self._parse_environment(stream) self.design.design_attributes.attributes.update(attributes or {}) self.add_objects_to_design(self.design, objects) obj_type, params = self._parse_command(stream) ## process net segments into nets & net points and add to design self.divide_segments() calculated_nets = self.calculate_nets() for cnet in sorted(calculated_nets, key=lambda n : n.net_id): self.design.add_net(cnet) return self.design def _parse_title_frame(self, params): """ Parse the frame component in *params* to extract the page size to be used in the design. The offset is adjusted according to the bottom-left position of the frame. """ ## set offset based on bottom-left corner of frame self.offset.x = params['x'] self.offset.y = params['y'] filename = self.known_symbols.get(params['basename']) if not filename or not os.path.exists(filename): log.warn("could not find title symbol '%s'" % params['basename']) self.frame_width = 46800 self.frame_height = 34000 return ## store title component name in design self.design.design_attributes.add_attribute( '_geda_titleframe', params['basename'], ) with open(filename, 'rU') as stream: obj_type, params = self._parse_command(stream) while obj_type is not None: if obj_type == 'B': if params['width'] > self.frame_width: self.frame_width = params['width'] if params['height'] > self.frame_height: self.frame_height = params['height'] ## skip commands covering multiple lines elif obj_type in ['T', 'H']: for _ in range(params['num_lines']): stream.readline() obj_type, params = self._parse_command(stream) ## set width to estimated max value when no box was found if self.frame_width == 0: self.frame_width = 46800 ## set height to estimated max value when no box was found if self.frame_height == 0: self.frame_height = 34000 def _create_ripper_segment(self, params): """ Creates a new segement from the busripper provided in gEDA. The busripper is a graphical feature that provides a nicer look for a part of a net. The bus rippers are turned into net segments according to the length and orientation in *params*. Returns a tuple of two NetPoint objects for the segment. """ x, y = params['x'], params['y'] angle, mirror = params['angle'], params['mirror'] if mirror: angle = (angle + 90) % 360 x, y = self.conv_coords(x, y) pt_a = self.get_netpoint(x, y) ripper_size = self.to_px(200) ## create second point for busripper segment on bus if angle == 0: pt_b = self.get_netpoint(pt_a.x+ripper_size, pt_a.y+ripper_size) elif angle == 90: pt_b = self.get_netpoint(pt_a.x-ripper_size, pt_a.y+ripper_size) elif angle == 180: pt_b = self.get_netpoint(pt_a.x-ripper_size, pt_a.y-ripper_size) elif angle == 270: pt_b = self.get_netpoint(pt_a.x+ripper_size, pt_a.y-ripper_size) else: raise GEDAError( "invalid angle in component '%s'" % params['basename'] ) return pt_a, pt_b def _parse_component(self, stream, params): """ Creates a component instance according to the component *params*. If the component is not known in the library, a the component will be created according to its description in the embedded environment ``[]`` or a symbol file. The component is added to the library automatically if necessary. An instance of this component will be created and added to the design. A GEDAError is raised when either the component file is invalid or the referenced symbol file cannot be found in the known directories. Returns a tuple of Component and ComponentInstance objects. """ basename, _ = os.path.splitext(params['basename']) component_name = basename if params.get('mirror'): component_name += '_MIRRORED' if component_name in self.design.components.components: component = self.design.components.components[component_name] ## skipping embedded data might be required self.skip_embedded_section(stream) else: ##check if sym file is embedded or referenced if basename.startswith('EMBEDDED'): ## embedded only has to be processed when NOT in symbol lookup if basename not in self.known_symbols: component = self.parse_component_data(stream, params) else: if basename not in self.known_symbols: log.warn("referenced symbol file '%s' unknown" % basename) ## create a unknown symbol reference component = self.parse_component_data( StringIO(UNKNOWN_COMPONENT % basename), params ) ## parse optional attached environment before continuing self._parse_environment(stream) return None, None ## requires parsing of referenced symbol file with open(self.known_symbols[basename], "rU") as f_in: self._check_version(f_in) component = self.parse_component_data(f_in, params) self.design.add_component(component_name, component) ## get all attributes assigned to component instance attributes = self._parse_environment(stream) ## refdes attribute is name of component (mandatory as of gEDA doc) ## examples if gaf repo have components without refdes, use part of ## basename if attributes is not None: instance = ComponentInstance( attributes.get('_refdes', component.name), component.name, 0 ) for key, value in attributes.items(): instance.add_attribute(key, value) else: instance = ComponentInstance( component.name, component.name, 0 ) ## generate a component instance using attributes self.design.add_component_instance(instance) symbol = SymbolAttribute( self.x_to_px(params['x']), self.y_to_px(params['y']), self.conv_angle(params['angle'], False) ) instance.add_symbol_attribute(symbol) ## add annotation for special attributes for idx, attribute_key in enumerate(['_refdes', 'device']): if attribute_key in component.attributes \ or attribute_key in instance.attributes: symbol.add_annotation( Annotation( '{{%s}}' % attribute_key, 0, 0+idx*10, 0.0, 'true' ) ) return component, instance def _check_version(self, stream): """ Check next line in *stream* for gEDA version data starting with ``v``. Raises ``GEDAError`` when no version data can be found. """ typ, _ = self._parse_command(stream) if typ != 'v': raise GEDAError( "cannot convert file, not in gEDA format" ) return True def _is_mirrored_command(self, params): return bool(params.get('mirror', False)) def parse_component_data(self, stream, params): """ Creates a component from the component *params* and the following commands in the stream. If the component data is embedded in the schematic file, all coordinates will be translated into the origin first. Only a single symbol/body is created for each component since gEDA symbols contain exactly one description. Returns the newly created Component object. """ # pylint: disable=R0912 basename = os.path.splitext(params['basename'])[0] saved_offset = self.offset self.offset = shape.Point(0, 0) ## retrieve if component is mirrored around Y-axis mirror = self._is_mirrored_command(params) if mirror: basename += '_MIRRORED' move_to = None if basename.startswith('EMBEDDED'): move_to = (params['x'], params['y']) ## grab next line (should be '[' typ, params = self._parse_command(stream, move_to) if typ == '[': typ, params = self._parse_command(stream, move_to) component = components.Component(basename) symbol = components.Symbol() component.add_symbol(symbol) body = components.Body() symbol.add_body(body) ##NOTE: adding this attribute to make parsing UPV data easier ## when using re-exported UPV. component.add_attribute('_geda_imported', 'true') self.pin_counter = itertools.count(0) while typ is not None: params['mirror'] = mirror objects = getattr(self, "_parse_%s" % typ)(stream, params) attributes = self._parse_environment(stream) component.attributes.update(attributes or {}) self.add_objects_to_component(component, objects) typ, params = self._parse_command(stream, move_to) self.offset = saved_offset return component def divide_segments(self): """ Checks all net segments for intersecting points of all other net segments. If an intersection is detected the net segment is divided into two segments with the intersecting point. This method has been adapted from a similar method in the kiCAD parser. """ ## check if segments need to be divided add_segs = set() rem_segs = set() for segment in self.segments: for point in self.net_points.values(): if self.intersects_segment(segment, point): pt_a, pt_b = segment rem_segs.add(segment) add_segs.add((pt_a, point)) add_segs.add((point, pt_b)) self.segments -= rem_segs self.segments |= add_segs def skip_embedded_section(self, stream): """ Reads the *stream* line by line until the end of an embedded section (``]``) is found. This method is used to skip over embedded sections of already known components. """ pos = stream.tell() typ = stream.readline().split(self.DELIMITER, 1)[0].strip() ## return with stream reset to previous position if not ## an embedded section if typ != '[': stream.seek(pos) return while typ != ']': typ = stream.readline().split(self.DELIMITER, 1)[0].strip() def get_netpoint(self, x, y): """ Creates a new NetPoint at coordinates *x*,*y* and stores it in the net point lookup table. If a NetPoint does already exist, the existing point is returned. Returns a NetPoint object at coordinates *x*,*y* """ if (x, y) not in self.net_points: self.net_points[(x, y)] = net.NetPoint('%da%d' % (x, y), x, y) return self.net_points[(x, y)] @staticmethod def intersects_segment(segment, pt_c): """ Checks if point *pt_c* lays on the *segment*. This code is adapted from the kiCAD parser. Returns True if *pt_c* is on *segment*, False otherwise. """ pt_a, pt_b = segment #check vertical segment if pt_a.x == pt_b.x == pt_c.x: if min(pt_a.y, pt_b.y) < pt_c.y < max(pt_a.y, pt_b.y): return True #check vertical segment elif pt_a.y == pt_b.y == pt_c.y: if min(pt_a.x, pt_b.x) < pt_c.x < max(pt_a.x, pt_b.x): return True #check diagonal segment elif (pt_c.x-pt_a.x)*(pt_b.y-pt_a.y) \ == (pt_b.x-pt_a.x)*(pt_c.y-pt_a.y): if min(pt_a.x, pt_b.x) < pt_c.x < max(pt_a.x, pt_b.x): return True ## point C not on segment return False def _parse_environment(self, stream): """ Checks if attribute environment starts in the next line (marked by '{'). Environment only contains text elements interpreted as text. Returns a dictionary of attributes. """ current_pos = stream.tell() typ, params = self._parse_command(stream) #go back to previous position when no environment in stream if typ != '{': stream.seek(current_pos) return None typ, params = self._parse_command(stream) attributes = {} while typ is not None: if typ == 'T': geda_text = self._parse_T(stream, params) if geda_text.is_attribute(): attributes[geda_text.attribute] = geda_text.content else: log.warn("normal text in environemnt does not comply " "with GEDA format specification: %s", geda_text.content) typ, params = self._parse_command(stream) return attributes def calculate_nets(self): """ Calculate connected nets from previously stored segments and netpoints. The code has been adapted from the kiCAD parser since the definition of segments in the schematic file are similar. The segments are checked against existing nets and added when they touch it. For this to work, it is required that intersecting segments are divided prior to this method. Returns a list of valid nets and its net points. """ nets = [] # Iterate over the segments, removing segments when added to a net while self.segments: seg = self.segments.pop() # pick a point net_name = '' pt_a, pt_b = seg if pt_a.point_id in self.net_names: net_name = self.net_names[pt_a.point_id] elif pt_b.point_id in self.net_names: net_name = self.net_names[pt_b.point_id] new_net = net.Net(net_name) new_net.connect(seg) found = True if net_name: new_net.attributes['_name'] = net_name while found: found = set() for seg in self.segments: # iterate over segments if new_net.connected(seg): # segment touching the net new_net.connect(seg) # add the segment found.add(seg) for seg in found: self.segments.remove(seg) nets.append(new_net) # check if names are available for calculated nets for net_obj in nets: for point_id in net_obj.points: ## check for stored net names based on pointIDs if point_id in self.net_names: net_obj.net_id = self.net_names[point_id] net_obj.attributes['_name'] = self.net_names[point_id] if '_name' in net_obj.attributes: annotation = Annotation( "{{_name}}", ## annotation referencing attribute '_name' 0, 0, self.conv_angle(0.0), self.conv_bool(1), ) net_obj.add_annotation(annotation) for net_obj in nets: if not net_obj.net_id: net_obj.net_id = min(net_obj.points) return nets def _open_file_or_zip(self, filename, mode='rU'): """ Open the file with *filename* and return a file handle for it. If the current file is a ZIP file the filename will be treated as compressed file in this ZIP file. """ if self.geda_zip is not None: temp_dir = tempfile.mkdtemp() self.geda_zip.extract(filename, temp_dir) filename = os.path.join(temp_dir, filename) return open(filename, mode) def add_text_to_component(self, component, geda_text): """ Add the content of a ``GEDAText`` instance to the component. If *geda_text* contains ``refdes``, ``prefix`` or ``suffix`` attributes it will be stored as special attribute in the component. *geda_text* that is not an attribute will be added as ``Label`` to the components body. """ if geda_text.is_text(): component.symbols[0].bodies[0].add_shape(geda_text.as_label()) elif geda_text.attribute == '_refdes' \ and '?' in geda_text.content: prefix, suffix = geda_text.content.split('?') component.add_attribute('_prefix', prefix) component.add_attribute('_suffix', suffix) else: component.add_attribute( geda_text.attribute, geda_text.content ) def add_objects_to_component(self, component, objs): """ Add a GEDA object to the component. Valid objects are subclasses of ``Shape``, ``Pin`` or ``GEDAText``. *objs* is expected to be an iterable and will be added to the correct component properties according to their type. """ if not objs: return try: iter(objs) except TypeError: objs = [objs] for obj in objs: obj_cls = obj.__class__ if issubclass(obj_cls, shape.Shape): component.symbols[0].bodies[0].add_shape(obj) elif issubclass(obj_cls, components.Pin): component.symbols[0].bodies[0].add_pin(obj) elif issubclass(obj_cls, GEDAText): self.add_text_to_component(component, obj) def add_text_to_design(self, design, geda_text): """ Add the content of a ``GEDAText`` instance to the design. If *geda_text* contains ``use_license`` it will be added to the design's metadata ``license`` other attributes are added to ``design_attributes``. *geda_text* that is not an attribute will be added as ``Label`` to the components body. """ if geda_text.is_text(): design.add_shape(geda_text.as_label()) elif geda_text.attribute == 'use_license': metadata = design.design_attributes.metadata metadata.license = geda_text.content else: design.design_attributes.add_attribute( geda_text.attribute, geda_text.content, ) def add_objects_to_design(self, design, objs): """ Add a GEDA object to the design. Valid objects are subclasses of ``Shape``, ``Pin`` or ``GEDAText``. *objs* is expected to be an iterable and will be added to the correct component properties according to their type. """ if not objs: return try: iter(objs) except TypeError: objs = [objs] for obj in objs: obj_cls = obj.__class__ if issubclass(obj_cls, shape.Shape): design.add_shape(obj) elif issubclass(obj_cls, components.Pin): design.add_pin(obj) elif issubclass(obj_cls, GEDAText): self.add_text_to_design(design, obj) def _parse_U(self, stream, params): """ Processing a bus instance with start end end coordinates at (x1, y1) and (x2, y2). *color* is ignored. *ripperdir* defines the direction in which the bus rippers are oriented relative to the direction of the bus. """ x1, x2 = params['x1'], params['x2'] y1, y2 = params['y1'], params['y2'] ## ignore bus when length is zero if x1 == x2 and y1 == y2: return pta_x, pta_y = self.conv_coords(x1, y1) ptb_x, ptb_y = self.conv_coords(x2, y2) self.segments.add(( self.get_netpoint(pta_x, pta_y), self.get_netpoint(ptb_x, ptb_y) )) def _parse_L(self, stream, params): """ Creates a Line object from the parameters in *params*. All style related parameters are ignored. Returns a Line object. """ line_x1 = params['x1'] line_x2 = params['x2'] if self._is_mirrored_command(params): line_x1 = 0 - params['x1'] line_x2 = 0 - params['x2'] line = shape.Line( self.conv_coords(line_x1, params['y1']), self.conv_coords(line_x2, params['y2']), ) ## store style data for line in 'style' dict self._save_parameters_to_object(line, params) return line def _parse_B(self, stream, params): """ Creates rectangle from gEDA box with origin in bottom left corner. All style related values are ignored. Returns a Rectangle object. """ rect_x = params['x'] if self._is_mirrored_command(params): rect_x = 0-(rect_x+params['width']) rect = shape.Rectangle( self.x_to_px(rect_x), self.y_to_px(params['y']+params['height']), self.to_px(params['width']), self.to_px(params['height']) ) ## store style data for rect in 'style' dict self._save_parameters_to_object(rect, params) return rect def _parse_V(self, stream, params): """ Creates a Circle object from the gEDA parameters in *params. All style related parameters are ignored. Returns a Circle object. """ vertex_x = params['x'] if self._is_mirrored_command(params): vertex_x = 0-vertex_x circle = shape.Circle( self.x_to_px(vertex_x), self.y_to_px(params['y']), self.to_px(params['radius']), ) ## store style data for arc in 'style' dict self._save_parameters_to_object(circle, params) return circle def _parse_A(self, stream, params): """ Creates an Arc object from the parameter in *params*. All style related parameters are ignored. Returns Arc object. """ arc_x = params['x'] start_angle = params['startangle'] sweep_angle = params['sweepangle'] if self._is_mirrored_command(params): arc_x = 0 - arc_x start_angle = start_angle + sweep_angle if start_angle <= 180: start_angle = 180 - start_angle else: start_angle = (360 - start_angle) + 180 arc = shape.Arc( self.x_to_px(arc_x), self.y_to_px(params['y']), self.conv_angle(start_angle), self.conv_angle(start_angle+sweep_angle), self.to_px(params['radius']), ) ## store style data for arc in 'style' dict self._save_parameters_to_object(arc, params) return arc def _parse_T(self, stream, params): """ Parses text element and determins if text is a text object or an attribute. Returns a tuple (key, value). If text is an annotation key is None. """ params['x'] = self.x_to_px(params['x']) params['y'] = self.y_to_px(params['y']) params['angle'] = self.conv_angle(params['angle']) geda_text = GEDAText.from_command(stream, params) ## text can have environemnt attached: parse & ignore self._parse_environment(stream) return geda_text def _parse_N(self, stream, params): """ Creates a segment from the command *params* and stores it in the global segment list for further processing in :py:method:divide_segments and :py:method:calculate_nets. It also extracts the net name from the attribute environment if present. """ ## store segement for processing later x1, y1 = self.conv_coords(params['x1'], params['y1']) x2, y2 = self.conv_coords(params['x2'], params['y2']) ## store segment points in global point list pt_a = self.get_netpoint(x1, y1) pt_b = self.get_netpoint(x2, y2) ## add segment to global list for later processing self.segments.add((pt_a, pt_b)) attributes = self._parse_environment(stream) if attributes is not None: ## create net with name in attributes if '_netname' in attributes: net_name = attributes['_netname'] if net_name not in self.net_names.values(): self.net_names[pt_a.point_id] = net_name def _parse_P(self, stream, params, pinnumber=0): """ Creates a Pin object from the parameters in *param* and text attributes provided in the following environment. The environment is enclosed in ``{}`` and is required. If no attributes can be extracted form *stream* an GEDAError is raised. The *pin_id* is retrieved from the 'pinnumber' attribute and all other attributes are ignored. The conneted end of the pin is taken from the 'whichend' parameter as defined in the gEDA documentation. Returns a Pin object. """ ## pin requires an attribute enviroment, so parse it first attributes = self._parse_environment(stream) if attributes is None: log.warn('mandatory pin attributes missing') attributes = { '_pinnumber': pinnumber, } if '_pinnumber' not in attributes: attributes['_pinnumber'] = pinnumber log.warn("mandatory attribute '_pinnumber' not assigned to pin") whichend = params['whichend'] pin_x1, pin_x2 = params['x1'], params['x2'] if self._is_mirrored_command(params): pin_x1 = 0-pin_x1 pin_x2 = 0-pin_x2 ## determine wich end of the pin is the connected end ## 0: first point is connector ## 1: second point is connector if whichend == 0: connect_end = self.conv_coords(pin_x1, params['y1']) null_end = self.conv_coords(pin_x2, params['y2']) else: null_end = self.conv_coords(pin_x1, params['y1']) connect_end = self.conv_coords(pin_x2, params['y2']) label = None if '_pinlabel' in attributes: label = shape.Label( connect_end[0], connect_end[1], attributes.get('_pinlabel'), 'left', 0.0 ) pin = components.Pin( attributes['_pinnumber'], #pin number null_end, connect_end, label=label ) ## store style parameters in shape's style dict self._save_parameters_to_object(pin, params) return pin def _parse_C(self, stream, params): """ Parse component command 'C'. *stream* is the file stream pointing to the line after the component command. *params* are the parsed parameters from the component command. The method checks if component is a title and ignores it if that is the case due to previous processing. If the component is a busripper, it is converted into a net segment. Otherwise, the component is parsed as a regular component and added to the library and design. """ ## ignore title since it only defines the blueprint frame if params['basename'].startswith('title'): self._parse_environment(stream) ## busripper are virtual components that need separate ## processing elif 'busripper' in params['basename']: self.segments.add(self._create_ripper_segment(params)) ## make sure following environments are ignored self.skip_embedded_section(stream) self._parse_environment(stream) else: self._parse_component(stream, params) def _parse_H(self, stream, params): """ Parses a SVG-like path provided path into a list of simple shapes. The gEDA formats allows only line and curve segments with absolute coordinates. Hence, shapes are either Line or BezierCurve objects. The method processes the stream data according to the number of lines in *params*. Returns a list of Line and BezierCurve shapes. """ params['extra_id'] = self.path_counter.next() num_lines = params['num_lines'] mirrored = self._is_mirrored_command(params) command = stream.readline().strip().split(self.DELIMITER) if command[0] != 'M': raise GEDAError('found invalid path in gEDA file') def get_coords(string, mirrored): """ Get coordinates from string with comma-sparated notation.""" x, y = [int(value) for value in string.strip().split(',')] if mirrored: x = -x return (self.x_to_px(x), self.y_to_px(y)) shapes = [] current_pos = initial_pos = (get_coords(command[1], mirrored)) ## loop over the remaining lines of commands (after 'M') for _ in range(num_lines-1): command = stream.readline().strip().split(self.DELIMITER) ## draw line from current to given position if command[0] == 'L': assert(len(command) == 2) end_pos = get_coords(command[1], mirrored) shape_ = shape.Line(current_pos, end_pos) current_pos = end_pos ## draw curve from current to given position elif command[0] == 'C': assert(len(command) == 4) control1 = get_coords(command[1], mirrored) control2 = get_coords(command[2], mirrored) end_pos = get_coords(command[3], mirrored) shape_ = shape.BezierCurve( control1, control2, current_pos, end_pos ) current_pos = end_pos ## end of sub-path, straight line from current to initial position elif command[0] in ['z', 'Z']: shape_ = shape.Line(current_pos, initial_pos) else: raise GEDAError( "invalid command type in path '%s'" % command[0] ) ## store style parameters in shape's style dict self._save_parameters_to_object(shape_, params) shapes.append(shape_) return shapes def _save_parameters_to_object(self, obj, params): """ Save all ``style`` and ``extra`` parameters to the objects ``styles`` dictionary. If *obj* does not have a ``styles`` property, a ``GEDAError`` is raised. """ parameter_types = [ geda_commands.GEDAStyleParameter.TYPE, geda_commands.GEDAExtraParameter.TYPE, ] try: for key, value in params.items(): if key.split('_')[0] in parameter_types: obj.styles[key] = value except AttributeError: log.exception( "tried saving style data to '%s' without styles dict.", obj.__class__.__name__ ) def _parse_command(self, stream, move_to=None): """ Parse the next command in *stream*. The object type is check for validity and its parameters are parsed and converted to the expected typs in the parsers lookup table. If *move_to* is provided it is used to translate all coordinates into by the given coordinate. Returns a tuple (*object type*, *parameters*) where *parameters* is a dictionary of paramter name and value. Raises GEDAError when object type is not known. """ line = stream.readline() while line.startswith('#') or line == '\n': line = stream.readline() command_data = line.strip().split(self.DELIMITER) if len(command_data[0]) == 0 or command_data[0] in [']', '}']: return None, [] object_type, command_data = command_data[0].strip(), command_data[1:] if object_type not in self.OBJECT_TYPES: raise GEDAError("unknown type '%s' in file" % object_type) params = {} geda_command = self.OBJECT_TYPES[object_type] for idx, parameter in enumerate(geda_command.parameters()): if idx >= len(command_data): ## prevent text commands of version 1 from breaking params[parameter.name] = parameter.default else: datatype = parameter.datatype params[parameter.name] = datatype(command_data[idx]) assert(len(params) == len(geda_command.parameters())) if move_to is not None: ## element in EMBEDDED component need to be moved ## to origin (0, 0) from component origin if object_type in ['T', 'B', 'C', 'A']: params['x'] = params['x'] - move_to[0] params['y'] = params['y'] - move_to[1] elif object_type in ['L', 'P']: params['x1'] = params['x1'] - move_to[0] params['y1'] = params['y1'] - move_to[1] params['x2'] = params['x2'] - move_to[0] params['y2'] = params['y2'] - move_to[1] return object_type, params @classmethod def to_px(cls, value): """ Converts value in MILS to pixels using the parsers scale factor. Returns an integer value converted to pixels. """ return int(value / cls.SCALE_FACTOR) def x_to_px(self, x_mils): """ Convert *px* from MILS to pixels using the scale factor and translating it allong the X-axis in offset. Returns translated and converted X coordinate. """ return int(float(x_mils - self.offset.x) / self.SCALE_FACTOR) def y_to_px(self, y_mils): """ Convert *py* from MILS to pixels using the scale factor and translating it allong the Y-axis in offset. Returns translated and converted Y coordinate. """ return int(float(y_mils - self.offset.y) / self.SCALE_FACTOR) def conv_coords(self, orig_x, orig_y): """ Converts coordinats *orig_x* and *orig_y* from MILS to pixel units based on scale factor. The converted coordinates are in multiples of 10px. """ orig_x, orig_y = int(orig_x), int(orig_y) return ( self.x_to_px(orig_x), self.y_to_px(orig_y) ) @staticmethod def conv_bool(value): """ Converts *value* into string representing boolean 'true' or 'false'. *value* can be of any numeric or boolean type. """ if value in ['true', 'false']: return value return str(bool(int(value)) is True).lower() @staticmethod def conv_angle(angle): """ Converts *angle* (in degrees) to pi radians. gEDA sets degree angles counter-clockwise whereas upverter uses pi radians clockwise. Therefore the direction of *angle* is therefore adjusted first. """ angle = angle % 360.0 if angle > 0: angle = abs(360 - angle) return round(angle/180.0, 1)
class EagleXML(object): """ The Eagle XML Format Parser. This parser uses code generated by generateDS.py which converts an xsd file to a set of python objects with parse and export functions. That code is in generated.py. It was created by the following steps: 1. Started with eagle.dtd from Eagle 6.2.0. 2. Removed inline comments in dtd (was breaking conversion to xsd). The dtd is also stored in this directory. 3. Converted to eagle.xsd using dtd2xsd.pl from w3c. The xsd is also stored in this directory. 4. Run a modified version of generateDS.py with the following arguments: --silence --external-encoding=utf-8 -o generated.py """ MULT = 90 / 25.4 # mm to 90 dpi def __init__(self): self.design = Design() # map components to gate names to symbol indices self.cpt2gate2symbol_index = defaultdict(dict) @staticmethod def auto_detect(filename): """ Return our confidence that the given file is an eagle xml schematic """ with open(filename, 'r') as f: data = f.read(4096) confidence = 0.0 if 'eagle.dtd' in data: confidence += 0.9 return confidence def parse(self, filename): """ Parse an Eagle XML file into a design """ root = parse(filename) self.make_components(root) self.make_component_instances(root) return self.design def make_components(self, root): """ Construct openjson components for an eagle model. """ for lib in get_subattr(root, 'drawing.schematic.libraries.library', ()): for deviceset in get_subattr(lib, 'devicesets.deviceset', ()): cpt = self.make_component(lib, deviceset) self.design.components.add_component(cpt.name, cpt) def make_component(self, lib, deviceset): """ Construct an openjson component for a deviceset in a library. """ cpt = Component(lib.name + ':' + deviceset.name) for gate in get_subattr(deviceset, 'gates.gate'): symbol = Symbol() cpt.add_symbol(symbol) self.cpt2gate2symbol_index[cpt][gate.name] = len(cpt.symbols) - 1 symbol.add_body(self.make_body_from_symbol(lib, gate.symbol)) return cpt def make_body_from_symbol(self, lib, symbol_name): """ Contruct an openjson Body from an eagle symbol in a library. """ body = Body() symbol = [s for s in get_subattr(lib, 'symbols.symbol') if s.name == symbol_name][0] for wire in symbol.wire: body.add_shape(Line((self.make_length(wire.x1), self.make_length(wire.y1)), (self.make_length(wire.x2), self.make_length(wire.y2)))) return body def make_component_instances(self, root): """ Construct openjson component instances for an eagle model. """ parts = dict((p.name, p) for p in get_subattr(root, 'drawing.schematic.parts.part', ())) for sheet in get_subattr(root, 'drawing.schematic.sheets.sheet', ()): for instance in get_subattr(sheet, 'instances.instance', ()): inst = self.make_component_instance(parts, instance) self.design.add_component_instance(inst) def make_component_instance(self, parts, instance): """ Construct an openjson component instance for an eagle instance. """ part = parts[instance.part] library_id = part.library + ':' + part.deviceset # TODO pick correct symbol index inst = ComponentInstance(instance.part, library_id, 0) # TODO handle mirror # TODO handle smashed? attr = SymbolAttribute(self.make_length(instance.x), self.make_length(instance.y), self.make_angle(instance.rot or '0')) inst.add_symbol_attribute(attr) return inst def make_length(self, value): """ Make an openjson length measurement from an eagle length. """ return int(round(float(value) * self.MULT)) def make_angle(self, value): """ Make an openjson angle measurement from an eagle angle. """ return float(value.lstrip('MSR')) / 180
class Fritzing(object): """ The Fritzing Format Parser Connection points in a fritzing file are identified by a 'module index' which references a component instance or a wire, and a 'connector id' which references a specific pin. Together the (index, connid) tuple uniquely identifies a connection point. """ def __init__(self): self.design = Design() self.body = None # This maps fritzing connector keys to (x, y) coordinates self.connkey2xy = {} # (index, connid) -> (x, y) # This maps fritzing component indices to ComponentInstances self.component_instances = {} # index -> ComponentInstance # Map connector keys to the list of connector keys they # are connected to. self.connects = {} # (index, connid) -> [(index, connid)] self.components = {} # idref -> ComponentParser self.fritzing_version = None self.fzz_zipfile = None # The ZipFile if we are parsing an fzz @staticmethod def auto_detect(filename): """ Return our confidence that the given file is an fritzing file """ with open(filename, 'r') as f: data = f.read(4096) confidence = 0 if 'fritzingVersion' in data: confidence += 0.9 elif filename.endswith('.fzz'): confidence += 0.9 if confidence == 0 and zipfile.is_zipfile(filename): zip_file = zipfile.ZipFile(filename) for name in zip_file.namelist(): if name.endswith('.fz'): confidence += 0.9 break zip_file.close() return confidence def parse(self, filename): """ Parse a Fritzing file into a design """ tree = self.make_tree(filename) self.fritzing_version = tree.getroot().get('fritzingVersion', '0') for element in tree.findall('instances/instance'): self.parse_instance(element) for idref, cpt_parser in self.components.iteritems(): self.design.add_component(idref, cpt_parser.component) for cptinst in self.component_instances.itervalues(): self.design.add_component_instance(cptinst) for net in self.build_nets(): self.design.add_net(net) return self.design def make_tree(self, filename): """ Return an ElementTree for the given file name. """ if zipfile.is_zipfile(filename): self.fzz_zipfile = zipfile.ZipFile(filename) fz_name = [name for name in self.fzz_zipfile.namelist() if name.endswith('.fz')][0] fz_file = self.fzz_zipfile.open(fz_name) else: fz_file = filename return ElementTree(file=fz_file) def parse_instance(self, instance): """ Parse a Fritzing instance block """ if instance.get('moduleIdRef') == 'WireModuleID': self.parse_wire(instance) else: self.parse_component_instance(instance) def parse_wire(self, inst): """ Parse a Fritzing wire instance """ view = inst.find('views/schematicView') if view is None: return index = inst.get('modelIndex') geom = view.find('geometry') origin_x, origin_y = get_x(geom), get_y(geom) conn_keys = [] for connects in view.findall('connectors/connector/connects/connect'): if connects.get('layer') == 'breadboardbreadboard': return for i, connector in enumerate(view.findall('connectors/connector'), 1): cid = connector.get('connectorId') self.connkey2xy[index, cid] = (origin_x + get_x(geom, 'x%d' % i), origin_y + get_y(geom, 'y%d' % i)) conn_keys.append((index, cid)) self.connects[index, cid] = \ [(c.get('modelIndex'), c.get('connectorId')) for c in connector.findall('connects/connect')] # connect wire ends to each other if len(conn_keys) >= 2: self.connects[conn_keys[0]].append(conn_keys[1]) self.connects[conn_keys[1]].append(conn_keys[0]) def ensure_component(self, inst): """ If we have not already done so, create the Component the given Fritzing instance is an instance of. Return the Component, or None if we cannot load it""" idref = inst.get('moduleIdRef') if idref in self.components: return self.components[idref] fzp_path = inst.get('path') if not fzp_path: return None if exists(fzp_path): fzp_file = fzp_path else: fzp_file = self.lookup_fzz_file(fzp_path, 'part') if not fzp_file: fzp_file = lookup_part(fzp_path, self.fritzing_version) if fzp_file is not None: fzp_path = fzp_file if not fzp_file: return None parser = ComponentParser(idref) parser.parse_fzp(fzp_file) if parser.image is not None: svg_file = self.lookup_fzz_file(parser.image, 'svg.schematic') if svg_file is None: fzp_dir = dirname(fzp_path) parts_dir = dirname(fzp_dir) svg_path = join(parts_dir, 'svg', basename(fzp_dir), parser.image) if exists(svg_path): svg_file = svg_path if svg_file is not None: parser.parse_svg(svg_file) self.components[idref] = parser return parser def lookup_fzz_file(self, path, prefix): """ Find a file in our fzz archive, if any """ if not self.fzz_zipfile: return None fzz_name = prefix + '.' + basename(path) try: self.fzz_zipfile.getinfo(fzz_name) except KeyError: return None else: return self.fzz_zipfile.open(fzz_name) def parse_component_instance(self, inst): """ Parse a Fritzing non-wire instance into a ComponentInstance """ view = inst.find('views/schematicView') if view is None: return if view.get('layer') == 'breadboardbreadboard': return cpt = self.ensure_component(inst) if cpt is None: return index = inst.get('modelIndex') idref = inst.get('moduleIdRef') title = inst.find('title').text geom = view.find('geometry') xform = geom.find('transform') x, y = float(geom.get('x', 0)), float(geom.get('y', 0)) if xform is None: rotation = 0.0 else: matrix = tuple(int(float(xform.get(key, 0))) for key in ('m11', 'm12', 'm21', 'm22')) x, y = rotate_component(cpt, matrix, x, y) rotation = MATRIX2ROTATION.get(matrix, 0.0) compinst = ComponentInstance(title, cpt, idref, 0) compinst.add_symbol_attribute( SymbolAttribute(make_x(x), make_y(y), rotation, False)) self.component_instances[index] = compinst def build_nets(self): """ Build the nets from the connects, points, and instances """ xy2point = {} # x, y -> NetPoint def get_point(connkey): """ Return a new or existing NetPoint for an (x,y) coordinate """ x, y = self.connkey2xy[connkey] if (x, y) not in xy2point: xy2point[x, y] = NetPoint('%da%d' % (x, y), x, y) return xy2point[x, y] # connector key -> NetPoint connkey2point = dict((ck, get_point(ck)) for ck in self.connkey2xy) todo = set(self.connects) # set([(connector key)]) point2net = {} # NetPoint -> Net nets = set() def get_net(point): """ Return a new or existing Net for a NetPoint. """ if point not in point2net: point2net[point] = Net(str(len(nets))) nets.add(point2net[point]) return point2net[point] def combine_nets(n1, n2): """Add net n2 into n1, get rid of n1.""" for point in n2.points.itervalues(): n1.add_point(point) point2net[point] = n1 nets.discard(n2) def connect(p1, p2): """ Connect two points in a net, maybe the same point """ net = get_net(p1) point2net[p1] = net net.add_point(p1) if point2net.get(p2, net) is not net: combine_nets(net, point2net[p2]) else: net.add_point(p2) point2net[p2] = net if p1 is p2: return if p2.point_id not in p1.connected_points: p1.connected_points.append(p2.point_id) if p1.point_id not in p2.connected_points: p2.connected_points.append(p1.point_id) def add_to_net(main_key): """ Update a net with a new set of connects """ todo.discard(main_key) main_point = connkey2point[main_key] connect(main_point, main_point) remaining = [] for conn_key in self.connects[main_key]: if conn_key in todo: remaining.append(conn_key) if conn_key in connkey2point: connect(main_point, connkey2point[conn_key]) elif conn_key[0] in self.component_instances: inst = self.component_instances[conn_key[0]] cpt_parser = self.components[inst.library_id] cpt_parser.connect_point(conn_key[1], inst, main_point) return remaining while todo: remaining = [todo.pop()] while remaining: remaining.extend(add_to_net(remaining.pop(0))) nets = sorted(nets, key = lambda n : int(n.net_id)) for i, net in enumerate(nets): net.net_id = str(i) return nets
class Fritzing(object): """ The Fritzing Format Parser Connection points in a fritzing file are identified by a 'module index' which references a component instance or a wire, and a 'connector id' which references a specific pin. Together the (index, connid) tuple uniquely identifies a connection point. """ def __init__(self): self.design = Design() self.body = None # This maps fritzing connector keys to (x, y) coordinates self.connkey2xy = {} # (index, connid) -> (x, y) # This maps fritzing component indices to ComponentInstances self.component_instances = {} # index -> ComponentInstance # Map connector keys to the list of connector keys they # are connected to. self.connects = {} # (index, connid) -> [(index, connid)] self.components = {} # idref -> ComponentParser self.fritzing_version = None self.fzz_zipfile = None # The ZipFile if we are parsing an fzz @staticmethod def auto_detect(filename): """ Return our confidence that the given file is an fritzing file """ with open(filename, 'r') as f: data = f.read(4096) confidence = 0 if 'fritzingVersion' in data: confidence += 0.9 elif filename.endswith('.fzz'): confidence += 0.9 if confidence == 0 and zipfile.is_zipfile(filename): zip_file = zipfile.ZipFile(filename) for name in zip_file.namelist(): if name.endswith('.fz'): confidence += 0.9 break zip_file.close() return confidence def parse(self, filename): """ Parse a Fritzing file into a design """ tree = self.make_tree(filename) self.fritzing_version = tree.getroot().get('fritzingVersion', '0') for element in tree.findall('instances/instance'): self.parse_instance(element) for idref, cpt_parser in self.components.iteritems(): self.design.add_component(idref, cpt_parser.component) for cptinst in self.component_instances.itervalues(): self.design.add_component_instance(cptinst) for net in self.build_nets(): self.design.add_net(net) return self.design def make_tree(self, filename): """ Return an ElementTree for the given file name. """ if zipfile.is_zipfile(filename): self.fzz_zipfile = zipfile.ZipFile(filename) fz_name = [name for name in self.fzz_zipfile.namelist() if name.endswith('.fz')][0] fz_file = self.fzz_zipfile.open(fz_name) else: fz_file = filename return ElementTree(file=fz_file) def parse_instance(self, instance): """ Parse a Fritzing instance block """ if instance.get('moduleIdRef') == 'WireModuleID': self.parse_wire(instance) else: self.parse_component_instance(instance) def parse_wire(self, inst): """ Parse a Fritzing wire instance """ view = inst.find('views/schematicView') if view is None: return #view2 = inst.find('views/breadboardView') #if view2 is not None: # return index = inst.get('modelIndex') geom = view.find('geometry') origin_x, origin_y = get_x(geom), get_y(geom) conn_keys = [] for connects in view.findall('connectors/connector/connects/connect'): if connects.get('layer') == 'breadboardbreadboard': return for i, connector in enumerate(view.findall('connectors/connector'), 1): cid = connector.get('connectorId') self.connkey2xy[index, cid] = (origin_x + get_x(geom, 'x%d' % i), origin_y + get_y(geom, 'y%d' % i)) conn_keys.append((index, cid)) self.connects[index, cid] = \ [(c.get('modelIndex'), c.get('connectorId')) for c in connector.findall('connects/connect')] # connect wire ends to each other if len(conn_keys) >= 2: self.connects[conn_keys[0]].append(conn_keys[1]) self.connects[conn_keys[1]].append(conn_keys[0]) def ensure_component(self, inst): """ If we have not already done so, create the Component the given Fritzing instance is an instance of. Return the Component, or None if we cannot load it""" idref = inst.get('moduleIdRef') if idref in self.components: return self.components[idref] fzp_path = inst.get('path') if not fzp_path: # print "Path not found", idref return None if exists(fzp_path): fzp_file = fzp_path else: fzp_file = self.lookup_fzz_file(fzp_path, 'part') if not fzp_file: fzp_file = lookup_part(fzp_path, self.fritzing_version) if fzp_file is not None: fzp_path = fzp_file if not fzp_file: # print "File not found", idref return None parser = ComponentParser(idref) parser.parse_fzp(fzp_file) if parser.image is not None: #print "Image Path found", idref svg_file = self.lookup_fzz_file(parser.image, 'svg.schematic') if svg_file is None: # print "Image not in fzz", idref #print fzp_path fzp_dir = dirname(fzp_path) #print fzp_dir pdb_dir = dirname(fzp_dir) #print pdb_dir fritzingroot_dir = dirname(pdb_dir) parts_dir = fritzingroot_dir + '/parts' #print parts_dir svg_path = join(parts_dir, 'svg', basename(fzp_dir),parser.image) #print basename(fzp_dir) if exists(svg_path): svg_file = svg_path #print svg_file if svg_file is not None: parser.parse_svg(svg_file) self.components[idref] = parser return parser def lookup_fzz_file(self, path, prefix): """ Find a file in our fzz archive, if any """ if not self.fzz_zipfile: return None fzz_name = prefix + '.' + basename(path) try: self.fzz_zipfile.getinfo(fzz_name) except KeyError: return None else: return self.fzz_zipfile.open(fzz_name) def parse_component_instance(self, inst): """ Parse a Fritzing non-wire instance into a ComponentInstance """ view = inst.find('views/schematicView') if view is None: return if view.get('layer') == 'breadboardbreadboard': return cpt = self.ensure_component(inst) if cpt is None: return index = inst.get('modelIndex') idref = inst.get('moduleIdRef') title = inst.find('title').text geom = view.find('geometry') xform = geom.find('transform') x, y = float(geom.get('x', 0)), float(geom.get('y', 0)) if xform is None: rotation = 0.0 else: matrix = tuple(int(float(xform.get(key, 0))) for key in ('m11', 'm12', 'm21', 'm22')) x, y = rotate_component(cpt, matrix, x, y) rotation = MATRIX2ROTATION.get(matrix, 0.0) compinst = ComponentInstance(title, cpt, idref, 0) compinst.add_symbol_attribute( SymbolAttribute(make_x(x), make_y(y), rotation, False)) self.component_instances[index] = compinst def build_nets(self): """ Build the nets from the connects, points, and instances """ xy2point = {} # x, y -> NetPoint def get_point(connkey): """ Return a new or existing NetPoint for an (x,y) coordinate """ x, y = self.connkey2xy[connkey] if (x, y) not in xy2point: xy2point[x, y] = NetPoint('%da%d' % (x, y), x, y) return xy2point[x, y] # connector key -> NetPoint connkey2point = dict((ck, get_point(ck)) for ck in self.connkey2xy) todo = set(self.connects) # set([(connector key)]) point2net = {} # NetPoint -> Net nets = set() def get_net(point): """ Return a new or existing Net for a NetPoint. """ if point not in point2net: point2net[point] = Net(str(len(nets))) nets.add(point2net[point]) return point2net[point] def combine_nets(n1, n2): """Add net n2 into n1, get rid of n1.""" for point in n2.points.itervalues(): n1.add_point(point) point2net[point] = n1 nets.discard(n2) def connect(p1, p2): """ Connect two points in a net, maybe the same point """ net = get_net(p1) point2net[p1] = net net.add_point(p1) if point2net.get(p2, net) is not net: combine_nets(net, point2net[p2]) else: net.add_point(p2) point2net[p2] = net if p1 is p2: return if p2.point_id not in p1.connected_points: p1.connected_points.append(p2.point_id) if p1.point_id not in p2.connected_points: p2.connected_points.append(p1.point_id) def add_to_net(main_key): """ Update a net with a new set of connects """ todo.discard(main_key) main_point = connkey2point[main_key] connect(main_point, main_point) remaining = [] for conn_key in self.connects[main_key]: if conn_key in todo: remaining.append(conn_key) if conn_key in connkey2point: connect(main_point, connkey2point[conn_key]) elif conn_key[0] in self.component_instances: inst = self.component_instances[conn_key[0]] cpt_parser = self.components[inst.library_id] cpt_parser.connect_point(conn_key[1], inst, main_point) return remaining while todo: remaining = [todo.pop()] while remaining: remaining.extend(add_to_net(remaining.pop(0))) nets = sorted(nets, key = lambda n : int(n.net_id)) for i, net in enumerate(nets): net.net_id = str(i) return nets
def test_layout(self): """ Capture absence of a layout. """ design = Design() writer = Writer() writer.write(design)
def parse(self, file_path): """ Parse an Altium file into a design """ design = Design() # Open the file in read-binary mode and only proceed if it was properly opened. in_file = open(file_path, "rb") if in_file: # Read the entire contents, omitting the interrupting blocks. input = in_file.read(0x200) # Skip the first 0x200 interrupting block. temp = in_file.read(0x200) while temp: # Read the next 0x10000 minus 0x200. temp = in_file.read(0xFE00) input += temp # Skip the next 0x200 interrupting block. temp = in_file.read(0x200) in_file.close() # Store all the headers, though they are not used. cursor_start = 0 self.first_header = input[cursor_start:cursor_start + 0x200] cursor_start += 0x200 self.root_entry = input[cursor_start:cursor_start + 0x80] cursor_start += 0x80 self.file_header = input[cursor_start:cursor_start + 0x80] cursor_start += 0x80 self.storage = input[cursor_start:cursor_start + 0x80] cursor_start += 0x80 self.additional = input[cursor_start:cursor_start + 0x80] cursor_start += 0x80 self.last_header = input[cursor_start:cursor_start + 0x200] cursor_start += 0x200 # Now prepare to read each of the parts. Initialize an "end" cursor. cursor_end = 0 # Get the size of the next part block. next_size = struct.unpack("<I", input[cursor_start:cursor_start + 4])[0] # Advance the "start" cursor. cursor_start += 4 # Create a list to store all the parts. self.parts = [] # Loop through until the "next size" is 0, which is the end of the parts list. while next_size != 0: cursor_end = input.find("\x00", cursor_start) # Create a dictionary to store all the property:value pairs. result = {} # Get a list of pairs by splitting on the separator character "|". property_list = input[cursor_start:cursor_end].split("|") # For each one, copy out whatever is before any "=" as property, and whatever is # after any "=" as value. for prop in property_list: if prop: property_val = p.split("=")[0] # The negative list index is to handle the cases with "==" instead of "=". value = p.split("=")[-1] # Add the property to the result dictionary. result[property_val] = value # Add the dictionary to the list of parts. self.parts.append(result) # Set things up for the next iteration of the loop. cursor_start = cursor_end + 1 next_size = struct.unpack( "<I", input[cursor_start:cursor_start + 4])[0] cursor_start += 4 # Here the footers could be found and stored, but I don't think they're important. return design
def parse(eagle_obj): design = Design()
def __init__(self): self.design = Design() # map components to gate names to symbol indices self.cpt2gate2symbol_index = defaultdict(dict)
class JSON(object): """ The Open JSON Format Parser This is mostly for sanity checks, it reads in the Open JSON format, and then outputs it. """ def __init__(self): self.design = Design() @staticmethod def auto_detect(filename): """ Return our confidence that the given file is an openjson file """ with open(filename, 'r') as f: data = f.read() confidence = 0 if 'component_instances' in data: confidence += 0.3 if 'design_attributes' in data: confidence += 0.6 return confidence def parse(self, filename): """ Parse the openjson file into the core. """ log.debug('Starting parse of %s', filename) with open(filename) as f: read = json.loads(f.read()) self.parse_components(read.get('components')) self.parse_component_instances(read.get('component_instances')) if read.get('shapes') is not None: self.parse_sch_shapes(read.get('shapes')) self.parse_design_attributes(read.get('design_attributes')) self.parse_nets(read.get('nets')) self.parse_version(read.get('version')) # layout aspects self.parse_layer_options(read.get('layer_options')) self.parse_trace_segments(read.get('trace_segments')) self.parse_layout_objects(read.get('gen_objs')) self.parse_paths(read.get('paths')) self.parse_pours(read.get('pours')) self.parse_pcb_text(read.get('text')) return self.design def parse_version(self, version): """ Extract the file version. """ file_version = version.get('file_version') exporter = version.get('exporter') self.design.set_version(file_version, exporter) def parse_layer_options(self, layer_options_json): if layer_options_json is None: return None for layer_option_json in layer_options_json: self.design.layer_options.append(Layer(layer_option_json['name'])) def parse_trace_segments(self, segments_json): if segments_json is None: return None for segment_json in segments_json: p1 = Point(segment_json['p1']['x'], segment_json['p1']['y']) p2 = Point(segment_json['p2']['x'], segment_json['p2']['y']) segment = Segment(segment_json['layer'], p1, p2, segment_json['width']) self.design.trace_segments.append(segment) def parse_paths(self, paths_json): if paths_json is None: return None for path_json in paths_json: points = [Point(point_json['x'], point_json['y']) for point_json in path_json['points']] width = path_json['width'] is_closed = path_json['is_closed'] layer = path_json['layer'] path = Path(layer, points, width, is_closed) self.design.paths.append(path) def parse_pours(self, pours_json): if pours_json is None: return None for pour_json in pours_json: points = [Point(point_json['x'], point_json['y']) for point_json in pour_json['points']] layer = pour_json['layer'] subtractive_shapes = []; if 'subtractive_shapes' in pour_json: subtractive_shapes = [self.parse_shape(shape_json) for shape_json in pour_json['subtractive_shapes']] if 'readded_shapes' in pour_json: readded_shapes = [self.parse_shape(shape_json) for shape_json in pour_json['readded_shapes']] pour = Pour(layer, points, subtractive_shapes, readded_shapes) self.design.pours.append(pour) def parse_pcb_text(self, text_json): if text_json is None: return None for text_instance_json in text_json: anno = self.parse_annotation(text_instance_json) self.design.pcb_text.append(anno) def parse_layout_objects(self, gen_objs_json): if gen_objs_json is None: return None for gen_obj_json in gen_objs_json: gen_obj = parse_gen_obj_json(gen_obj_json) self.design.layout_objects.append(gen_obj) def parse_component_instances(self, component_instances): """ Extract the component instances. """ if component_instances is None: return None for instance in component_instances: # Get instance_id, library_id and symbol_index instance_id = instance.get('instance_id') library_id = instance.get('library_id') symbol_index = int(instance.get('symbol_index')) footprint_index = int(instance.get('footprint_index')) # Make the ComponentInstance() inst = ComponentInstance(instance_id, self.design.components.components[library_id], library_id, symbol_index, footprint_index) # Get the SymbolAttributes for symbol_attribute in instance.get('symbol_attributes', []): attr = self.parse_symbol_attribute(symbol_attribute) inst.add_symbol_attribute(attr) # TODO(shamer) footprint_pos, fleb and genobj positions are relative to the footprint_pos for footprint_attribute in instance.get('footprint_attributes', []): attr = self.parse_footprint_attribute(footprint_attribute) inst.add_footprint_attribute(attr) for gen_obj_attribute in instance.get('gen_obj_attributes', []): attr = self.parse_gen_obj_attribute(gen_obj_attribute) inst.add_gen_obj_attribute(attr) footprint_json = instance.get('footprint_pos') if footprint_json: footprint_pos = self.parse_footprint_pos(footprint_json) else: footprint_pos = None inst.set_footprint_pos(footprint_pos) # Get the Attributes for key, value in instance.get('attributes').items(): inst.add_attribute(key, value) # Add the ComponentInstance self.design.add_component_instance(inst) def parse_symbol_attribute(self, symbol_attribute): """ Extract attributes from a symbol. """ x = int(symbol_attribute.get('x') or 0) y = int(symbol_attribute.get('y') or 0) rotation = float(symbol_attribute.get('rotation')) try: flip = (symbol_attribute.get('flip').lower() == "true") except: flip = False # Make SymbolAttribute symbol_attr = SymbolAttribute(x, y, rotation, flip) # Add Annotations for annotation in symbol_attribute.get('annotations'): anno = self.parse_annotation(annotation) symbol_attr.add_annotation(anno) # Return SymbolAttribute to be added to its ComponentInstance return symbol_attr def parse_footprint_attribute(self, footprint_attribute): """ Extract attributes from a footprint. """ x = int(footprint_attribute.get('x') or 0) y = int(footprint_attribute.get('y') or 0) rotation = float(footprint_attribute.get('rotation')) try: flip = (footprint_attribute.get('flip').lower() == "true") except: flip = False layer = footprint_attribute.get('layer') footprint_attr = FootprintAttribute(x, y, rotation, flip, layer) return footprint_attr def parse_gen_obj_attribute(self, gen_obj_attribute): """ Extract attributes from a gen_obj. """ x = int(gen_obj_attribute.get('x') or 0) y = int(gen_obj_attribute.get('y') or 0) rotation = float(gen_obj_attribute.get('rotation')) try: flip = (gen_obj_attribute.get('flip').lower() == "true") except: flip = False layer = gen_obj_attribute.get('layer') gen_obj_attr = GenObjAttribute(x, y, rotation, flip, layer) for key, value in gen_obj_attribute.get('attributes').items(): gen_obj_attr.add_attribute(key, value) return gen_obj_attr def parse_footprint_pos(self, footprint_pos_json): """ Extract footprint pos. """ x = int(footprint_pos_json.get('x') or 0) y = int(footprint_pos_json.get('y') or 0) rotation = float(footprint_pos_json.get('rotation', 0)) flip = footprint_pos_json.get('flip') side = footprint_pos_json.get('side') return FootprintPos(x, y, rotation, flip, side) def parse_annotation(self, annotation): """ Extract an annotation. """ value = annotation.get('value') x = int(annotation.get('x')) y = int(annotation.get('y')) label = self.parse_label(annotation.get('label')) layer = annotation.get('layer', 'default') rotation = float(annotation.get('rotation')) flip_horizontal = annotation.get('flip', False) visible = annotation.get('visible') if visible is not None and visible.lower() == 'false': visible = 'false' else: visible = 'true' return Annotation(value, x, y, rotation, visible, layer=layer, flip_horizontal=flip_horizontal, label=label) def parse_components(self, components): """ Extract a component library. """ for library_id, component in components.items(): name = component.get('name') comp = Component(name) # Get attributes for key, value in component.get('attributes', []).items(): comp.add_attribute(key, value) for symbol_json in component.get('symbols', []): symbol = self.parse_symbol(symbol_json) comp.add_symbol(symbol) for footprint_json in component.get('footprints', []): footprint = self.parse_footprint(footprint_json) comp.add_footprint(footprint) self.design.add_component(library_id, comp) def parse_sch_shapes(self, shapes): """ Extract shapes drawn directly on the schematic. """ for shape in shapes: self.design.add_shape(self.parse_shape(shape)) def parse_symbol(self, symbol_json): """ Extract a symbol. """ symb = Symbol() for body in symbol_json.get('bodies'): bdy = self.parse_symbol_body(body) symb.add_body(bdy) return symb def parse_footprint(self, footprint_json): """ Extract the bodies for a footprint. """ footprint = Footprint() for body_json in footprint_json.get('bodies'): body = self.parse_footprint_body(body_json) footprint.add_body(body) for gen_obj_json in footprint_json.get('gen_objs'): gen_obj = self.parse_gen_obj(gen_obj_json) footprint.add_gen_obj(gen_obj) return footprint def parse_gen_obj(self, gen_obj_json): """ Extract the generated object. """ gen_obj = parse_gen_obj_json(gen_obj_json) return gen_obj def parse_footprint_body(self, body_json): """ Extract a body of a symbol. """ body = FBody() for shape in body_json.get('shapes'): parsed_shape = self.parse_shape(shape) body.add_shape(parsed_shape) body.layer = body_json.get('layer') body.rotation = body_json.get('rotation', 0) body.flip_horizontal = body_json.get('flip_horizontal', False) return body def parse_symbol_body(self, body): """ Extract a body of a symbol. """ bdy = SBody() pins = body.get('pins') if (pins != None): for pin in body.get('pins'): parsed_pin = self.parse_pin(pin) bdy.add_pin(parsed_pin) shapes = body.get('shapes') if (shapes != None): for shape in body.get('shapes'): parsed_shape = self.parse_shape(shape) bdy.add_shape(parsed_shape) return bdy def parse_pin(self, pin): """ Extract a pin of a body. """ pin_number = pin.get('pin_number') p1 = self.parse_point(pin.get('p1')) p2 = self.parse_point(pin.get('p2')) parsed_pin = Pin(pin_number, p1, p2) if pin.get('label') is not None: parsed_pin.label = self.parse_label(pin.get('label')) parsed_pin.styles = pin.get('styles') or {} return parsed_pin def parse_point(self, point): """ Extract a point. """ x = int(point.get('x')) y = int(point.get('y')) return Point(x, y) def parse_label(self, label): """ Extract a label. """ if label is None: return None x = int(label.get('x')) y = int(label.get('y')) text = label.get('text') font_size = label.get('font_size') font_family = label.get('font_family') align = label.get('align') baseline = label.get('baseline') rotation = float(label.get('rotation')) parsed_label = Label(x, y, text, font_size, font_family, align, baseline, rotation) parsed_label.styles = label.get('styles') or {} return parsed_label def parse_shape(self, shape): """ Extract a shape. """ # pylint: disable=R0914 # pylint: disable=R0911 rotation = shape.get('rotation', 0.0) flip_horizontal = shape.get('flip_horizontal', False) shape_type = shape.get('type') if 'rectangle' == shape_type: x = int(shape.get('x')) y = int(shape.get('y')) height = int(shape.get('height')) width = int(shape.get('width')) parsed_shape = Rectangle(x, y, width, height) elif 'rounded_rectangle' == shape_type: x = int(shape.get('x')) y = int(shape.get('y')) height = int(shape.get('height')) width = int(shape.get('width')) radius = int(shape.get('radius')) parsed_shape = RoundedRectangle(x, y, width, height, radius) elif 'arc' == shape_type: x = int(shape.get('x')) y = int(shape.get('y')) start_angle = float(shape.get('start_angle')) end_angle = float(shape.get('end_angle')) radius = int(shape.get('radius')) parsed_shape = Arc(x, y, start_angle, end_angle, radius) elif 'circle' == shape_type: x = int(shape.get('x')) y = int(shape.get('y')) radius = int(shape.get('radius')) parsed_shape = Circle(x, y, radius) elif 'label' == shape_type: parsed_shape = self.parse_label(shape) elif 'line' == shape_type: p1 = self.parse_point(shape.get('p1')) p2 = self.parse_point(shape.get('p2')) parsed_shape = Line(p1, p2) elif 'polygon' == shape_type: parsed_shape = Polygon() for point in shape.get('points'): parsed_shape.add_point(self.parse_point(point)) elif 'bezier' == shape_type: control1 = self.parse_point(shape.get('control1')) control2 = self.parse_point(shape.get('control2')) p1 = self.parse_point(shape.get('p1')) p2 = self.parse_point(shape.get('p2')) parsed_shape = BezierCurve(control1, control2, p1, p2) elif 'rounded_segment' == shape_type: p1 = self.parse_point(shape.get('p1')) p2 = self.parse_point(shape.get('p2')) width = int(shape.get('width')) parsed_shape = RoundedSegment(p1, p2, width) parsed_shape.rotation = rotation parsed_shape.flip_horizontal = flip_horizontal parsed_shape.styles = shape.get('styles') or {} parsed_shape.attributes = shape.get('attributes') or {} return parsed_shape def parse_design_attributes(self, design_attributes): """ Extract design attributes. """ attrs = DesignAttributes() # Get the Annotations for annotation in design_attributes.get('annotations'): anno = self.parse_annotation(annotation) attrs.add_annotation(anno) # Get the Attributes for key, value in design_attributes.get('attributes').items(): attrs.add_attribute(key, value) # Get the Metadata meta = self.parse_metadata(design_attributes.get('metadata')) attrs.set_metadata(meta) self.design.set_design_attributes(attrs) def parse_metadata(self, metadata): """ Extract design meta-data. """ meta = Metadata() meta.set_name(metadata.get('name')) meta.set_license(metadata.get('license')) meta.set_owner(metadata.get('owner')) meta.set_updated_timestamp(metadata.get('updated_timestamp')) meta.set_design_id(metadata.get('design_id')) meta.set_description(metadata.get('description')) meta.set_slug(metadata.get('slug')) for attached_url in metadata.get('attached_urls'): meta.add_attached_url(attached_url) return meta def parse_nets(self, nets): """ Extract nets. """ for net in nets: net_id = net.get('net_id') ret_net = Net(net_id) # Add Annotations for annotation in net.get('annotations'): anno = self.parse_annotation(annotation) ret_net.add_annotation(anno) # Get the Attributes for key, value in net.get('attributes').items(): ret_net.add_attribute(key, value) # Get the Points for net_point in net.get('points'): npnt = self.parse_net_point(net_point) ret_net.add_point(npnt) self.design.add_net(ret_net) def parse_net_point(self, net_point): """ Extract a net point. """ point_id = net_point.get('point_id') x = int(net_point.get('x')) y = int(net_point.get('y')) npnt = NetPoint(point_id, x, y) # Get the connected points for point in net_point.get('connected_points'): npnt.add_connected_point(point) # Get the ConnectedComponents comps = net_point.get('connected_components') if (comps != None): for connectedcomponent in comps: conn_comp = self.parse_connected_component(connectedcomponent) npnt.add_connected_component(conn_comp) return npnt def parse_connected_component(self, connectedcomponent): """ Extract a connected component. """ instance_id = connectedcomponent.get('instance_id') pin_number = connectedcomponent.get('pin_number') return ConnectedComponent(instance_id, pin_number)
def setUp(self): """ Setup the test case. """ self.des = Design()
def parse_schematic(self, stream): """ Parse a gEDA schematic provided as a *stream* object into a design. Returns the design corresponding to the schematic. """ # pylint: disable=R0912 if self.design is None: self.design = Design() self.segments = set() self.net_points = dict() self.net_names = dict() obj_type, params = self._parse_command(stream) while obj_type is not None: if obj_type == 'T': ##Convert regular text or attribute key, value = self._parse_text(stream, params) if key is None: ## text is annotation self.design.design_attributes.add_annotation( self._create_annotation(value, params) ) elif key == 'use_license': self.design.design_attributes.metadata.license = value else: self.design.design_attributes.add_attribute(key, value) elif obj_type == 'G' : ## picture type is not supported log.warn("ignoring picture/font in gEDA file. Not supported!") elif obj_type == 'C': ## command for component found basename = params['basename'] ## ignore title since it only defines the blueprint frame if basename.startswith('title'): self._parse_environment(stream) ## busripper are virtual components that need separate ## processing elif 'busripper' in basename: self.segments.add( self._create_ripper_segment(params) ) ## make sure following environments are ignored self.skip_embedded_section(stream) self._parse_environment(stream) else: self.parse_component(stream, params) elif obj_type == 'N': ## net segment (in schematic ONLY) self._parse_segment(stream, params) elif obj_type == 'H': ## SVG-like path log.warn('ommiting path outside of component.') ## skip description of path num_lines = params['num_lines'] for _ in range(num_lines): stream.readline() elif obj_type == 'U': ## bus (only graphical feature NOT component) self._parse_bus(params) obj_type, params = self._parse_command(stream) ## process net segments into nets & net points and add to design self.divide_segments() calculated_nets = self.calculate_nets() for cnet in calculated_nets: self.design.add_net(cnet) return self.design
def parse(self, inputfile): """ Parse a gEDA file into a design. Returns the design corresponding to the gEDA file. """ inputfiles = [] ## check if inputfile is in ZIP format if zipfile.is_zipfile(inputfile): self.geda_zip = zipfile.ZipFile(inputfile) for filename in self.geda_zip.namelist(): if filename.endswith('.sch'): inputfiles.append(filename) else: inputfiles = [inputfile] self.design = Design() self.unassigned_body = components.Body() ## parse frame data of first schematic to extract ## page size (assumes same frame for all files) with self._open_file_or_zip(inputfiles[0]) as stream: self._check_version(stream) for line in stream.readlines(): if 'title' in line and line.startswith('C'): obj_type, params = self._parse_command(StringIO(line)) assert(obj_type == 'C') params['basename'], _ = os.path.splitext( params['basename'], ) log.debug("using title file: %s", params['basename']) self._parse_title_frame(params) for filename in inputfiles: f_in = self._open_file_or_zip(filename) self._check_version(f_in) self.parse_schematic(f_in) basename, _ = os.path.splitext(os.path.basename(filename)) self.design.design_attributes.metadata.set_name(basename) ## modify offset for next page to be shifted to the right self.offset.x = self.offset.x - self.frame_width f_in.close() ## if unassigned shapes have been found during parsing add a new ## component to the design if len(self.unassigned_body.shapes + self.unassigned_body.pins) > 0: component = components.Component("UNASSIGNED_SHAPES") symbol = components.Symbol() component.add_symbol(symbol) symbol.add_body(self.unassigned_body) instance = ComponentInstance(component.name, component.name, 0) symbol = SymbolAttribute(0, 0, 0) instance.add_symbol_attribute(symbol) self.design.add_component(component.name, component) self.design.add_component_instance(instance) return self.design
class JSON(object): """ The Open JSON Format Parser This is mostly for sanity checks, it reads in the Open JSON format, and then outputs it. """ def __init__(self): self.design = Design() @staticmethod def auto_detect(filename): """ Return our confidence that the given file is an openjson file """ with open(filename, 'r') as f: data = f.read() confidence = 0 if 'component_instances' in data: confidence += 0.3 if 'design_attributes' in data: confidence += 0.6 return confidence def parse(self, filename): """ Parse the openjson file into the core. """ log.debug('Starting parse of %s', filename) with open(filename) as f: read = json.loads(f.read()) self.parse_components(read.get('components')) self.parse_component_instances(read.get('component_instances')) if read.get('shapes') is not None: self.parse_sch_shapes(read.get('shapes')) self.parse_design_attributes(read.get('design_attributes')) self.parse_nets(read.get('nets')) self.parse_version(read.get('version')) # layout aspects self.parse_layer_options(read.get('layer_options')) self.parse_trace_segments(read.get('trace_segments')) self.parse_layout_objects(read.get('gen_objs')) self.parse_paths(read.get('paths')) self.parse_pours(read.get('pours')) self.parse_pcb_text(read.get('text')) return self.design def parse_version(self, version): """ Extract the file version. """ file_version = version.get('file_version') exporter = version.get('exporter') self.design.set_version(file_version, exporter) def parse_layer_options(self, layer_options_json): if layer_options_json is None: return None for layer_option_json in layer_options_json: self.design.layer_options.append(Layer(layer_option_json['name'])) def parse_trace_segments(self, segments_json): if segments_json is None: return None for segment_json in segments_json: p1 = Point(segment_json['p1']['x'], segment_json['p1']['y']) p2 = Point(segment_json['p2']['x'], segment_json['p2']['y']) segment = Segment(segment_json['layer'], p1, p2, segment_json['width']) self.design.trace_segments.append(segment) def parse_paths(self, paths_json): if paths_json is None: return None for path_json in paths_json: points = [ Point(point_json['x'], point_json['y']) for point_json in path_json['points'] ] width = path_json['width'] is_closed = path_json['is_closed'] layer = path_json['layer'] path = Path(layer, points, width, is_closed) self.design.paths.append(path) def parse_pours(self, pours_json): if pours_json is None: return None for pour_json in pours_json: points = [ Point(point_json['x'], point_json['y']) for point_json in pour_json['points'] ] layer = pour_json['layer'] subtractive_shapes = [] if 'subtractive_shapes' in pour_json: subtractive_shapes = [ self.parse_shape(shape_json) for shape_json in pour_json['subtractive_shapes'] ] if 'readded_shapes' in pour_json: readded_shapes = [ self.parse_shape(shape_json) for shape_json in pour_json['readded_shapes'] ] pour = Pour(layer, points, subtractive_shapes, readded_shapes) self.design.pours.append(pour) def parse_pcb_text(self, text_json): if text_json is None: return None for text_instance_json in text_json: anno = self.parse_annotation(text_instance_json) self.design.pcb_text.append(anno) def parse_layout_objects(self, gen_objs_json): if gen_objs_json is None: return None for gen_obj_json in gen_objs_json: gen_obj = parse_gen_obj_json(gen_obj_json) self.design.layout_objects.append(gen_obj) def parse_component_instances(self, component_instances): """ Extract the component instances. """ if component_instances is None: return None for instance in component_instances: # Get instance_id, library_id and symbol_index instance_id = instance.get('instance_id') library_id = instance.get('library_id') symbol_index = int(instance.get('symbol_index')) footprint_index = int(instance.get('footprint_index')) # Make the ComponentInstance() inst = ComponentInstance( instance_id, self.design.components.components[library_id], library_id, symbol_index, footprint_index) # Get the SymbolAttributes for symbol_attribute in instance.get('symbol_attributes', []): attr = self.parse_symbol_attribute(symbol_attribute) inst.add_symbol_attribute(attr) # TODO(shamer) footprint_pos, fleb and genobj positions are relative to the footprint_pos for footprint_attribute in instance.get('footprint_attributes', []): attr = self.parse_footprint_attribute(footprint_attribute) inst.add_footprint_attribute(attr) for gen_obj_attribute in instance.get('gen_obj_attributes', []): attr = self.parse_gen_obj_attribute(gen_obj_attribute) inst.add_gen_obj_attribute(attr) footprint_json = instance.get('footprint_pos') if footprint_json: footprint_pos = self.parse_footprint_pos(footprint_json) else: footprint_pos = None inst.set_footprint_pos(footprint_pos) # Get the Attributes for key, value in instance.get('attributes').items(): inst.add_attribute(key, value) # Add the ComponentInstance self.design.add_component_instance(inst) def parse_symbol_attribute(self, symbol_attribute): """ Extract attributes from a symbol. """ x = int(symbol_attribute.get('x') or 0) y = int(symbol_attribute.get('y') or 0) rotation = float(symbol_attribute.get('rotation')) try: flip = (symbol_attribute.get('flip').lower() == "true") except: flip = False # Make SymbolAttribute symbol_attr = SymbolAttribute(x, y, rotation, flip) # Add Annotations for annotation in symbol_attribute.get('annotations'): anno = self.parse_annotation(annotation) symbol_attr.add_annotation(anno) # Return SymbolAttribute to be added to its ComponentInstance return symbol_attr def parse_footprint_attribute(self, footprint_attribute): """ Extract attributes from a footprint. """ x = int(footprint_attribute.get('x') or 0) y = int(footprint_attribute.get('y') or 0) rotation = float(footprint_attribute.get('rotation')) try: flip = (footprint_attribute.get('flip').lower() == "true") except: flip = False layer = footprint_attribute.get('layer') footprint_attr = FootprintAttribute(x, y, rotation, flip, layer) return footprint_attr def parse_gen_obj_attribute(self, gen_obj_attribute): """ Extract attributes from a gen_obj. """ x = int(gen_obj_attribute.get('x') or 0) y = int(gen_obj_attribute.get('y') or 0) rotation = float(gen_obj_attribute.get('rotation')) try: flip = (gen_obj_attribute.get('flip').lower() == "true") except: flip = False layer = gen_obj_attribute.get('layer') gen_obj_attr = GenObjAttribute(x, y, rotation, flip, layer) for key, value in gen_obj_attribute.get('attributes').items(): gen_obj_attr.add_attribute(key, value) return gen_obj_attr def parse_footprint_pos(self, footprint_pos_json): """ Extract footprint pos. """ x = int(footprint_pos_json.get('x') or 0) y = int(footprint_pos_json.get('y') or 0) rotation = float(footprint_pos_json.get('rotation', 0)) flip = footprint_pos_json.get('flip') side = footprint_pos_json.get('side') return FootprintPos(x, y, rotation, flip, side) def parse_annotation(self, annotation): """ Extract an annotation. """ value = annotation.get('value') x = int(annotation.get('x')) y = int(annotation.get('y')) label = self.parse_label(annotation.get('label')) layer = annotation.get('layer', 'default') rotation = float(annotation.get('rotation')) flip_horizontal = annotation.get('flip', False) visible = annotation.get('visible') if visible is not None and visible.lower() == 'false': visible = 'false' else: visible = 'true' return Annotation(value, x, y, rotation, visible, layer=layer, flip_horizontal=flip_horizontal, label=label) def parse_components(self, components): """ Extract a component library. """ for library_id, component in components.items(): name = component.get('name') comp = Component(name) # Get attributes for key, value in component.get('attributes', []).items(): comp.add_attribute(key, value) for symbol_json in component.get('symbols', []): symbol = self.parse_symbol(symbol_json) comp.add_symbol(symbol) for footprint_json in component.get('footprints', []): footprint = self.parse_footprint(footprint_json) comp.add_footprint(footprint) self.design.add_component(library_id, comp) def parse_sch_shapes(self, shapes): """ Extract shapes drawn directly on the schematic. """ for shape in shapes: self.design.add_shape(self.parse_shape(shape)) def parse_symbol(self, symbol_json): """ Extract a symbol. """ symb = Symbol() for body in symbol_json.get('bodies'): bdy = self.parse_symbol_body(body) symb.add_body(bdy) return symb def parse_footprint(self, footprint_json): """ Extract the bodies for a footprint. """ footprint = Footprint() for body_json in footprint_json.get('bodies'): body = self.parse_footprint_body(body_json) footprint.add_body(body) for gen_obj_json in footprint_json.get('gen_objs'): gen_obj = self.parse_gen_obj(gen_obj_json) footprint.add_gen_obj(gen_obj) return footprint def parse_gen_obj(self, gen_obj_json): """ Extract the generated object. """ gen_obj = parse_gen_obj_json(gen_obj_json) return gen_obj def parse_footprint_body(self, body_json): """ Extract a body of a symbol. """ body = FBody() for shape in body_json.get('shapes'): parsed_shape = self.parse_shape(shape) body.add_shape(parsed_shape) body.layer = body_json.get('layer') body.rotation = body_json.get('rotation', 0) body.flip_horizontal = body_json.get('flip_horizontal', False) return body def parse_symbol_body(self, body): """ Extract a body of a symbol. """ bdy = SBody() for pin in body.get('pins'): parsed_pin = self.parse_pin(pin) bdy.add_pin(parsed_pin) for shape in body.get('shapes'): parsed_shape = self.parse_shape(shape) bdy.add_shape(parsed_shape) return bdy def parse_pin(self, pin): """ Extract a pin of a body. """ pin_number = pin.get('pin_number') p1 = self.parse_point(pin.get('p1')) p2 = self.parse_point(pin.get('p2')) parsed_pin = Pin(pin_number, p1, p2) if pin.get('label') is not None: parsed_pin.label = self.parse_label(pin.get('label')) parsed_pin.styles = pin.get('styles') or {} return parsed_pin def parse_point(self, point): """ Extract a point. """ x = int(point.get('x')) y = int(point.get('y')) return Point(x, y) def parse_label(self, label): """ Extract a label. """ if label is None: return None x = int(label.get('x')) y = int(label.get('y')) text = label.get('text') font_size = label.get('font_size') font_family = label.get('font_family') align = label.get('align') baseline = label.get('baseline') rotation = float(label.get('rotation')) parsed_label = Label(x, y, text, font_size, font_family, align, baseline, rotation) parsed_label.styles = label.get('styles') or {} return parsed_label def parse_shape(self, shape): """ Extract a shape. """ # pylint: disable=R0914 # pylint: disable=R0911 rotation = shape.get('rotation', 0.0) flip_horizontal = shape.get('flip_horizontal', False) shape_type = shape.get('type') if 'rectangle' == shape_type: x = int(shape.get('x')) y = int(shape.get('y')) height = int(shape.get('height')) width = int(shape.get('width')) parsed_shape = Rectangle(x, y, width, height) elif 'rounded_rectangle' == shape_type: x = int(shape.get('x')) y = int(shape.get('y')) height = int(shape.get('height')) width = int(shape.get('width')) radius = int(shape.get('radius')) parsed_shape = RoundedRectangle(x, y, width, height, radius) elif 'arc' == shape_type: x = int(shape.get('x')) y = int(shape.get('y')) start_angle = float(shape.get('start_angle')) end_angle = float(shape.get('end_angle')) radius = int(shape.get('radius')) parsed_shape = Arc(x, y, start_angle, end_angle, radius) elif 'circle' == shape_type: x = int(shape.get('x')) y = int(shape.get('y')) radius = int(shape.get('radius')) parsed_shape = Circle(x, y, radius) elif 'label' == shape_type: parsed_shape = self.parse_label(shape) elif 'line' == shape_type: p1 = self.parse_point(shape.get('p1')) p2 = self.parse_point(shape.get('p2')) parsed_shape = Line(p1, p2) elif 'polygon' == shape_type: parsed_shape = Polygon() for point in shape.get('points'): parsed_shape.add_point(self.parse_point(point)) elif 'bezier' == shape_type: control1 = self.parse_point(shape.get('control1')) control2 = self.parse_point(shape.get('control2')) p1 = self.parse_point(shape.get('p1')) p2 = self.parse_point(shape.get('p2')) parsed_shape = BezierCurve(control1, control2, p1, p2) elif 'rounded_segment' == shape_type: p1 = self.parse_point(shape.get('p1')) p2 = self.parse_point(shape.get('p2')) width = int(shape.get('width')) parsed_shape = RoundedSegment(p1, p2, width) parsed_shape.rotation = rotation parsed_shape.flip_horizontal = flip_horizontal parsed_shape.styles = shape.get('styles') or {} parsed_shape.attributes = shape.get('attributes') or {} return parsed_shape def parse_design_attributes(self, design_attributes): """ Extract design attributes. """ attrs = DesignAttributes() # Get the Annotations for annotation in design_attributes.get('annotations'): anno = self.parse_annotation(annotation) attrs.add_annotation(anno) # Get the Attributes for key, value in design_attributes.get('attributes').items(): attrs.add_attribute(key, value) # Get the Metadata meta = self.parse_metadata(design_attributes.get('metadata')) attrs.set_metadata(meta) self.design.set_design_attributes(attrs) def parse_metadata(self, metadata): """ Extract design meta-data. """ meta = Metadata() meta.set_name(metadata.get('name')) meta.set_license(metadata.get('license')) meta.set_owner(metadata.get('owner')) meta.set_updated_timestamp(metadata.get('updated_timestamp')) meta.set_design_id(metadata.get('design_id')) meta.set_description(metadata.get('description')) meta.set_slug(metadata.get('slug')) for attached_url in metadata.get('attached_urls'): meta.add_attached_url(attached_url) return meta def parse_nets(self, nets): """ Extract nets. """ for net in nets: net_id = net.get('net_id') ret_net = Net(net_id) # Add Annotations for annotation in net.get('annotations'): anno = self.parse_annotation(annotation) ret_net.add_annotation(anno) # Get the Attributes for key, value in net.get('attributes').items(): ret_net.add_attribute(key, value) # Get the Points for net_point in net.get('points'): npnt = self.parse_net_point(net_point) ret_net.add_point(npnt) self.design.add_net(ret_net) def parse_net_point(self, net_point): """ Extract a net point. """ point_id = net_point.get('point_id') x = int(net_point.get('x')) y = int(net_point.get('y')) npnt = NetPoint(point_id, x, y) # Get the connected points for point in net_point.get('connected_points'): npnt.add_connected_point(point) # Get the ConnectedComponents for connectedcomponent in net_point.get('connected_components'): conn_comp = self.parse_connected_component(connectedcomponent) npnt.add_connected_component(conn_comp) return npnt def parse_connected_component(self, connectedcomponent): """ Extract a connected component. """ instance_id = connectedcomponent.get('instance_id') pin_number = connectedcomponent.get('pin_number') return ConnectedComponent(instance_id, pin_number)
class Specctra(object): """ The Specctra DSN Format Parser """ def __init__(self): self.design = None self.resolution = None self.nets = {} self.net_points = {} self._id = 10 self.min_x = maxint self.max_x = -(maxint - 1) self.min_y = maxint self.max_y = -(maxint - 1) @staticmethod def auto_detect(filename): """ Return our confidence that the given file is an specctra schematic """ with open(filename, 'r') as f: data = f.read(4096) confidence = 0 if '(pcb ' in data or '(PCB ' in data: confidence += 0.75 return confidence def parse(self, filename): """ Parse a specctra file into a design """ self.design = Design() with open(filename) as f: data = f.read() tree = DsnParser().parse(data) struct = self.walk(tree) self.resolution = struct.resolution self._convert(struct) return self.design def _convert(self, struct): for bound in struct.structure.boundary: if bound.rectangle.layer_id == 'pcb': vertex1, vertex2 = bound.rectangle.vertex1, bound.rectangle.vertex2 self.min_x = self.to_pixels(min(vertex1[0], vertex2[0])) self.max_x = self.to_pixels(max(vertex1[0], vertex2[0])) self.min_y = self.to_pixels(min(vertex1[1], vertex2[1])) self.max_y = self.to_pixels(max(vertex1[1], vertex2[1])) break self._convert_library(struct) self._convert_components(struct) self._convert_nets(struct) def _convert_library(self, struct): """ Convert library """ for image in struct.library.image: component = Component(image.image_id) self.design.add_component(image.image_id, component) fpt = Footprint() body = FBody() component.add_footprint(fpt) fpt.add_body(body) for pad in image.pin: body.add_pad(Pad(pad.pad_id, self.to_pixels(pad.vertex), self.to_pixels(pad.vertex))) for padstack in struct.library.padstack: if padstack.padstack_id == pad.padstack_id: shapes = [shape.shape for shape in padstack.shape] for shape in self._convert_shapes(shapes, self.to_pixels(pad.vertex)): body.add_shape(shape) break for outline in image.outline: for shape in self._convert_shapes([outline.shape]): body.add_shape(shape) def _convert_components(self, struct): """ Convert component """ for component in struct.placement.component: library_id = component.image_id for place in component.place: # Outside PCB boundary if not place.vertex: continue mirror = {90:270, 270:90} if place.side == 'back': rotation = place.rotation else: rotation = mirror.get(int(place.rotation), place.rotation) inst = ComponentInstance(place.component_id, component, library_id, 0) v = self.to_pixels(place.vertex) symbattr = FootprintAttribute(v[0], v[1], to_piradians(rotation), False) inst.add_symbol_attribute(symbattr) self.design.add_component_instance(inst) def _get_point(self, net_id, point_id, x, y): if net_id not in self.nets: n = Net(net_id) self.design.add_net(n) self.nets[n.net_id] = n else: n = self.nets[net_id] key = (x, y) if key not in self.net_points: if not point_id: point_id = str(self._id) self._id += 1 np = NetPoint(net_id + '-' + point_id, x, y) n.add_point(np) self.net_points[key] = np else: np = self.net_points[key] return np def _convert_wires(self, struct): if struct.wiring: for wire in struct.wiring.wire: lines = self._convert_shapes([wire.shape], absolute=True) for line in lines: try: np1 = self._get_point(wire.net.net_id, None, line.p1.x, line.p1.y) np2 = self._get_point(wire.net.net_id, None, line.p2.x, line.p2.y) np1.add_connected_point(np2.point_id) np2.add_connected_point(np1.point_id) except: pass def _convert_nets(self, struct): """ Convert nets """ # FIXME polyline_path is not documented and no success with reverse engineering yet self._convert_wires(struct) if struct.network: for net in struct.network.net: if net.pins is None: continue prev_point = None for pin_ref in net.pins.pin_reference: # pin_ref like A1-"-" is valid (parsed to A1--) component_id, pin_id = pin_ref[:pin_ref.index('-')], pin_ref[pin_ref.index('-') + 1:] point = self.get_component_pin(component_id, pin_id) if point is None: print 'Could not find net %s pin ref %s' % (net.net_id, pin_ref) continue cc = ConnectedComponent(component_id, pin_id) np = self._get_point(net.net_id, pin_ref, point[0], point[1]) np.add_connected_component(cc) if prev_point is not None: # XXX if point is already connected assume wiring had routed network, don't do it here if len(prev_point.connected_points) == 0: prev_point.add_connected_point(np.point_id) if len(np.connected_points) == 0: np.add_connected_point(prev_point.point_id) prev_point = np def get_component_pin(self, component_id, pin_id): for component_instance in self.design.component_instances: symbattr = component_instance.symbol_attributes[0] if component_instance.instance_id == component_id: component = self.design.components.components[component_instance.library_id] for pin in component.symbols[0].bodies[0].pins: if pin.pin_number == pin_id: x, y = rotate((pin.p1.x, pin.p1.y), symbattr.rotation) return (symbattr.x + x, symbattr.y + y) def _make_line(self, p1, p2, aperture): x1, y1 = float(p1[0]), float(p1[1]) x2, y2 = float(p2[0]), float(p2[1]) aperture = float(aperture) dx = x2 - x1 dy = y2 - y1 length = math.sqrt(dx * dx + dy * dy) if length == 0.0: return [] result = [] line1 = Line( (x1 - aperture * (y2 - y1) / length, y1 - aperture * (x1 - x2) / length), (x2 - aperture * (y2 - y1) / length, y2 - aperture * (x1 - x2) / length) ) line2 = Line( (x1 + aperture * (y2 - y1) / length, y1 + aperture * (x1 - x2) / length), (x2 + aperture * (y2 - y1) / length, y2 + aperture * (x1 - x2) / length) ) result.append(line1) result.append(line2) def make_arc(p1, p2, p0): start_angle = math.atan2(p1.y - p0.y, p1.x - p0.x) / math.pi end_angle = math.atan2(p2.y - p0.y, p2.x - p0.x) / math.pi return Arc(p0.x, p0.y, start_angle, end_angle, aperture) result.append(make_arc(line1.p1, line2.p1, Point(p1))) result.append(make_arc(line2.p2, line1.p2, Point(p2))) return result def _convert_path(self, aperture, points): """ Convert path """ result = [] prev = points[0] for point in points[1:]: line = self._make_line(prev, point, float(aperture) / 2.0) result.extend(line) prev = point return result def _convert_shapes(self, shapes, center = (0, 0), absolute=False): """ Convert shapes """ result = [] def fix_point(point): x, y = (point[0] + center[0], point[1] + center[1]) if absolute: # freerouter often creates points outside boundary, fix it if x > self.max_x: x = self.min_x + x - self.max_x elif x < self.min_x: x = self.max_x - x - self.min_x if y > self.max_y: y = self.min_y + y - self.max_y elif y < self.min_y: y = self.max_y - y - self.min_y return (x, y) for shape in shapes: if isinstance(shape, specctraobj.PolylinePath): points = [fix_point(self.to_pixels(point)) for point in shape.vertex] result.extend(self._convert_path(self.to_pixels(shape.aperture_width), points)) elif isinstance(shape, specctraobj.Path): points = [fix_point(self.to_pixels(point)) for point in shape.vertex] # Path has connected start and end points if points[0] != points[-1] and len(points) != 2: points.append(points[0]) result.extend(self._convert_path(self.to_pixels(shape.aperture_width), points)) elif isinstance(shape, specctraobj.Polygon): points = [fix_point(self.to_pixels(point)) for point in shape.vertex] points = [Point(point[0], point[1]) for point in points] result.append(Polygon(points)) elif isinstance(shape, specctraobj.Rectangle): x1, y1 = self.to_pixels(shape.vertex1) x2, y2 = self.to_pixels(shape.vertex2) width, height = abs(x1 - x2), abs(y1 - y2) x1, y1 = fix_point((min(x1, x2), max(y1, y2))) result.append(Rectangle(x1, y1, width, height)) elif isinstance(shape, specctraobj.Circle): point = fix_point(self.to_pixels(shape.vertex)) result.append(Circle(point[0], point[1], self.to_pixels(shape.diameter / 2.0))) return result def to_pixels(self, vertex): return self.resolution.to_pixels(vertex) def walk(self, elem): if isinstance(elem, list) and len(elem) > 0: elemx = [self.walk(x) for x in elem] func = specctraobj.lookup(elemx[0]) if func: f = func() f.parse(elemx[1:]) return f else: #print 'Unhandled element', elemx[0] return elemx else: return elem
class EagleXML(object): """ The Eagle XML Format Parser. This parser uses code generated by generateDS.py which converts an xsd file to a set of python objects with parse and export functions. That code is in generated.py. It was created by the following steps: 1. Started with eagle.dtd from Eagle 6.2.0. 2. Removed inline comments in dtd (was breaking conversion to xsd). The dtd is also stored in this directory. 3. Converted to eagle.xsd using dtd2xsd.pl from w3c. The xsd is also stored in this directory. 4. Run a modified version of generateDS.py with the following arguments: --silence --external-encoding=utf-8 -o generated.py """ SCALE = 2.0 MULT = 90 / 25.4 # mm to 90 dpi def __init__(self): self.design = Design() # map (component, gate name) to body indices self.cptgate2body_index = {} # map (component, gate name) to pin maps, dicts from strings # (pin names) to Pins. These are used during pinref processing # in segments. self.cptgate2pin_map = defaultdict(dict) # map (component, gate names) to annotation maps, dicts from # strings (name|value) to Annotations. These represent the # >NAME and >VALUE texts on eagle components, which must be # converted into component instance annotations since their # contents depend on the component instance name and value. self.cptgate2ann_map = defaultdict(dict) # map part names to component instances. These are used during # pinref processing in segments. self.part2inst = {} # map part names to gate names to symbol attributes. These # are used during pinref processing in segments. self.part2gate2symattr = defaultdict(dict) @staticmethod def auto_detect(filename): """ Return our confidence that the given file is an eagle xml schematic """ with open(filename, 'r') as f: data = f.read(4096) confidence = 0.0 if 'eagle.dtd' in data: confidence += 0.9 return confidence def parse(self, filename): """ Parse an Eagle XML file into a design """ root = parse(filename) self.make_components(root) self.make_component_instances(root) self.make_nets(root) self.design.scale(EAGLE_SCALE) return self.design def make_components(self, root): """ Construct openjson components from an eagle model. """ for lib in get_subattr(root, 'drawing.schematic.libraries.library', ()): for deviceset in get_subattr(lib, 'devicesets.deviceset', ()): for cpt in self.make_deviceset_components(lib, deviceset): self.design.components.add_component(cpt.name, cpt) def make_deviceset_components(self, lib, deviceset): """ Construct openjson components for each device in an eaglexml deviceset in a library.""" for device in deviceset.devices.device: yield self.make_device_component(lib, deviceset, device) def make_device_component(self, lib, deviceset, device): """ Construct an openjson component for a device in a deviceset. """ cpt = Component(lib.name + ':' + deviceset.name + ':' + device.name) cpt.add_attribute('eaglexml_library', lib.name) cpt.add_attribute('eaglexml_deviceset', deviceset.name) cpt.add_attribute('eaglexml_device', device.name) symbol = Symbol() cpt.add_symbol(symbol) assignment = PinNumberAssignment(device) for i, gate in enumerate(get_subattr(deviceset, 'gates.gate')): body, pin_map, ann_map = self.make_body_from_symbol( lib, gate.symbol, assignment.get_pin_number_lookup(gate.name)) symbol.add_body(body) cpt.add_attribute('eaglexml_symbol_%d' % i, gate.symbol) cpt.add_attribute('eaglexml_gate_%d' % i, gate.name) self.cptgate2body_index[cpt, gate.name] = len(symbol.bodies) - 1 self.cptgate2pin_map[cpt, gate.name] = pin_map self.cptgate2ann_map[cpt, gate.name] = ann_map return cpt def make_body_from_symbol(self, lib, symbol_name, pin_number_lookup): """ Construct an openjson SBody from an eagle symbol in a library. """ body = SBody() symbol = [s for s in get_subattr(lib, 'symbols.symbol') if s.name == symbol_name][0] for wire in symbol.wire: body.add_shape(self.make_shape_for_wire(wire)) for rect in symbol.rectangle: rotation = make_angle('0' if rect.rot is None else rect.rot) x1, y1 = rotate_point((self.make_length(rect.x1), self.make_length(rect.y1)), rotation) x2, y2 = rotate_point((self.make_length(rect.x2), self.make_length(rect.y2)), rotation) ux, uy = min(x1, x2), max(y1, y2) lx, ly = max(x1, x2), min(y1, y2) body.add_shape(Rectangle(ux, uy, lx - ux, uy - ly)) for poly in symbol.polygon: map(body.add_shape, self.make_shapes_for_poly(poly)) for circ in symbol.circle: body.add_shape(self.make_shape_for_circle(circ)) pin_map = {} for pin in symbol.pin: connect_point = (self.make_length(pin.x), self.make_length(pin.y)) null_point = self.get_pin_null_point(connect_point, pin.length, pin.rot) label = self.get_pin_label(pin, null_point) pin_map[pin.name] = Pin(pin_number_lookup(pin.name), null_point, connect_point, label) if pin.direction: pin_map[pin.name].add_attribute('eaglexml_direction', pin.direction) if pin.visible: pin_map[pin.name].add_attribute('eaglexml_visible', pin.visible) body.add_pin(pin_map[pin.name]) ann_map = {} for text in symbol.text: x = self.make_length(text.x) y = self.make_length(text.y) content = '' if text.valueOf_ is None else text.valueOf_ rotation = make_angle('0' if text.rot is None else text.rot) align = 'right' if is_mirrored(text.rot) else 'left' if rotation == 0.5: rotation = 1.5 if content.lower() == '>name': ann_map['name'] = Annotation(content, x, y, rotation, 'true') elif content.lower() == '>value': ann_map['value'] = Annotation(content, x, y, rotation, 'true') else: body.add_shape(Label(x, y, content, align=align, rotation=rotation)) return body, pin_map, ann_map def make_shape_for_wire(self, wire): """ Generate an openjson shape for an eaglexml wire. """ if wire.curve is None: return Line((self.make_length(wire.x1), self.make_length(wire.y1)), (self.make_length(wire.x2), self.make_length(wire.y2))) curve, x1, y1, x2, y2 = map(float, (wire.curve, wire.x1, wire.y1, wire.x2, wire.y2)) if curve < 0: curve = -curve negative = True mult = -1.0 else: negative = False mult = 1.0 if curve > 180.0: major_arc = True curve = 360.0 - curve mult *= -1.0 else: major_arc = False chordlen = sqrt(pow(x2 - x1, 2) + pow(y2 - y1, 2)) radius = chordlen / (2.0 * sin(radians(curve) / 2)) mx, my = (x1 + x2) / 2, (y1 + y2) / 2 # midpoint between arc points h = sqrt(pow(radius, 2) - pow(chordlen / 2, 2)) # height of isoceles # calculate center point cx = mx + mult * h * (y1 - y2) / chordlen cy = my + mult * h * (x2 - x1) / chordlen if negative: start_angle = atan2(y2 - cy, x2 - cx) end_angle = start_angle + radians(curve) - (pi if major_arc else 0.0) else: start_angle = atan2(y1 - cy, x1 - cx) end_angle = start_angle + radians(curve) + (pi if major_arc else 0.0) return Arc(self.make_length(cx), self.make_length(cy), round(start_angle / pi, 3) % 2.0, round(end_angle / pi, 3) % 2.0, self.make_length(radius)) def make_shapes_for_poly(self, poly): """ Generate openjson shapes for an eaglexml polygon. """ # TODO: handle curves opoly = Polygon() for vertex in poly.vertex: opoly.add_point(self.make_length(vertex.x), self.make_length(vertex.y)) yield opoly def make_shape_for_circle(self, circ): """ Generate an openjson shape for an eaglexml circle. """ ocirc = Circle(self.make_length(circ.x), self.make_length(circ.y), self.make_length(circ.radius)) ocirc.add_attribute('eaglexml_width', circ.width) return ocirc def get_pin_null_point(self, (x, y), length, rotation): """ Return the null point of a pin given its connect point, length, and rotation. """ if length == 'long': distance = int(27 * self.SCALE) # .3 inches elif length == 'middle': distance = int(18 * self.SCALE) # .2 inches elif length == 'short': distance = int(9 * self.SCALE) # .1 inches else: # point distance = 0 if rotation is None: rotation = "" if rotation.endswith('R90'): coords = (x, y + distance) elif rotation.endswith('R180'): coords = (x - distance, y) elif rotation.endswith('R270'): coords = (x, y - distance) else: coords = (x + distance, y) if is_mirrored(rotation): x, y = coords coords = (-x, y) return coords
class EagleXML(object): """ The Eagle XML Format Parser. This parser uses code generated by generateDS.py which converts an xsd file to a set of python objects with parse and export functions. That code is in generated.py. It was created by the following steps: 1. Started with eagle.dtd from Eagle 6.2.0. 2. Removed inline comments in dtd (was breaking conversion to xsd). The dtd is also stored in this directory. 3. Converted to eagle.xsd using dtd2xsd.pl from w3c. The xsd is also stored in this directory. 4. Run a modified version of generateDS.py with the following arguments: --silence --external-encoding=utf-8 -o generated.py """ SCALE = 2.0 MULT = 90 / 25.4 # mm to 90 dpi def __init__(self): self.design = Design() # map (component, gate name) to body indices self.cptgate2body_index = {} # map (component, gate name) to pin maps, dicts from strings # (pin names) to Pins. These are used during pinref processing # in segments. self.cptgate2pin_map = defaultdict(dict) # map (component, gate names) to annotation maps, dicts from # strings (name|value) to Annotations. These represent the # >NAME and >VALUE texts on eagle components, which must be # converted into component instance annotations since their # contents depend on the component instance name and value. self.cptgate2ann_map = defaultdict(dict) # map part names to component instances. These are used during # pinref processing in segments. self.part2inst = {} # map part names to gate names to symbol attributes. These # are used during pinref processing in segments. self.part2gate2symattr = defaultdict(dict) @staticmethod def auto_detect(filename): """ Return our confidence that the given file is an eagle xml schematic """ with open(filename, 'r') as f: data = f.read(4096) confidence = 0.0 if 'eagle.dtd' in data: confidence += 0.9 return confidence def parse(self, filename): """ Parse an Eagle XML file into a design """ root = parse(filename) self.make_components(root) self.make_component_instances(root) self.make_nets(root) self.design.scale(EAGLE_SCALE) return self.design def make_components(self, root): """ Construct openjson components from an eagle model. """ for lib in get_subattr(root, 'drawing.schematic.libraries.library', ()): for deviceset in get_subattr(lib, 'devicesets.deviceset', ()): cpt = self.make_deviceset_component(lib, deviceset) self.design.components.add_component(cpt.name, cpt) def make_deviceset_component(self, lib, deviceset): """ Construct an openjson component for an eaglexml deviceset in a library.""" cpt = Component(lib.name + ':' + deviceset.name + ':logical') cpt.add_attribute('eaglexml_type', 'logical') cpt.add_attribute('eaglexml_library', lib.name) cpt.add_attribute('eaglexml_deviceset', deviceset.name) symbol = Symbol() cpt.add_symbol(symbol) for i, gate in enumerate(get_subattr(deviceset, 'gates.gate')): body, pin_map, ann_map = self.make_body_from_symbol(lib, gate.symbol) symbol.add_body(body) cpt.add_attribute('eaglexml_symbol_%d' % i, gate.symbol) cpt.add_attribute('eaglexml_gate_%d' % i, gate.name) self.cptgate2body_index[cpt, gate.name] = len(symbol.bodies) - 1 self.cptgate2pin_map[cpt, gate.name] = pin_map self.cptgate2ann_map[cpt, gate.name] = ann_map return cpt def make_body_from_symbol(self, lib, symbol_name): """ Construct an openjson Body from an eagle symbol in a library. """ body = Body() symbol = [s for s in get_subattr(lib, 'symbols.symbol') if s.name == symbol_name][0] for wire in symbol.wire: body.add_shape(Line((self.make_length(wire.x1), self.make_length(wire.y1)), (self.make_length(wire.x2), self.make_length(wire.y2)))) for rect in symbol.rectangle: x = self.make_length(rect.x1) y = self.make_length(rect.y1) width = self.make_length(rect.x2) - x height = self.make_length(rect.y2) - y body.add_shape(Rectangle(x, y + height, width, height)) for poly in symbol.polygon: map(body.add_shape, self.make_shapes_for_poly(poly)) for circ in symbol.circle: body.add_shape(self.make_shape_for_circle(circ)) pin_map = {} for pin in symbol.pin: connect_point = (self.make_length(pin.x), self.make_length(pin.y)) null_point = self.get_pin_null_point(connect_point, pin.length, pin.rot) label = self.get_pin_label(pin, null_point) pin_map[pin.name] = Pin(pin.name, null_point, connect_point, label) if pin.direction: pin_map[pin.name].add_attribute('eaglexml_direction', pin.direction) if pin.visible: pin_map[pin.name].add_attribute('eaglexml_visible', pin.visible) body.add_pin(pin_map[pin.name]) ann_map = {} for text in symbol.text: x = self.make_length(text.x) y = self.make_length(text.y) content = '' if text.valueOf_ is None else text.valueOf_ rotation = self.make_angle('0' if text.rot is None else text.rot) if content == '>NAME': ann_map['name'] = Annotation(content, x, y, rotation, 'true') elif content == '>VALUE': ann_map['value'] = Annotation(content, x, y, rotation, 'true') else: body.add_shape(Label(x, y, content, 'left', rotation)) return body, pin_map, ann_map def make_shapes_for_poly(self, poly): """ Generate openjson shapes for an eaglexml polygon. """ # TODO: handle curves opoly = Polygon() for vertex in poly.vertex: opoly.add_point(self.make_length(vertex.x), self.make_length(vertex.y)) yield opoly def make_shape_for_circle(self, circ): """ Generate an openjson shape for an eaglexml circle. """ ocirc = Circle(self.make_length(circ.x), self.make_length(circ.y), self.make_length(circ.radius)) ocirc.add_attribute('eaglexml_width', circ.width) return ocirc def get_pin_null_point(self, (x, y), length, rotation): """ Return the null point of a pin given its connect point, length, and rotation. """ if length == 'long': distance = int(27 * self.SCALE) # .3 inches elif length == 'middle': distance = int(18 * self.SCALE) # .2 inches elif length == 'short': distance = int(9 * self.SCALE) # .1 inches else: # point distance = 0 if rotation is None: rotation = "" if rotation.endswith('R90'): coords = (x, y + distance) elif rotation.endswith('R180'): coords = (x - distance, y) elif rotation.endswith('R270'): coords = (x, y - distance) else: coords = (x + distance, y) if rotation.startswith('M'): x, y = coords coords = (-x, y) return coords
def test_layers(self): """ Capture absence of layers. """ design = Design() design.layout = Layout() writer = Writer() writer.write(design)
class GEDA: """ The GEDA Format Parser """ DELIMITER = ' ' SCALE_FACTOR = 10.0 # maps 1000 MILS to 10 pixels OBJECT_TYPES = { 'v': geda_commands.GEDAVersionCommand(), 'L': geda_commands.GEDALineCommand(), 'B': geda_commands.GEDABoxCommand(), 'V': geda_commands.GEDACircleCommand(), 'A': geda_commands.GEDAArcCommand(), 'T': geda_commands.GEDATextCommand(), 'N': geda_commands.GEDASegmentCommand(), 'U': geda_commands.GEDABusCommand(), 'P': geda_commands.GEDAPinCommand(), 'C': geda_commands.GEDAComponentCommand(), 'H': geda_commands.GEDAPathCommand(), ## valid types but are ignored 'G': geda_commands.GEDAPictureCommand(), ## environments '{': geda_commands.GEDAEmbeddedEnvironmentCommand(), '}': [], # attributes '[': geda_commands.GEDAAttributeEnvironmentCommand(), ']': [], # embedded component } def __init__(self, symbol_dirs=None): """ Constuct a gEDA parser object. Specifying a list of symbol directories in *symbol_dir* will provide a symbol file lookup in the specified directories. The lookup will be generated instantly examining each directory (if it exists). Kwargs: symbol_dirs (list): List of directories containing .sym files """ self.offset = shape.Point(40000, 40000) ## Initialise frame size with largest possible size self.frame_width = 0 self.frame_height = 0 # initialise PIN counter self.pin_counter = itertools.count(0) # initialise PATH counter self.path_counter = itertools.count(0) ## add flag to allow for auto inclusion if symbol_dirs is None: symbol_dirs = [] symbol_dirs = symbol_dirs + \ [os.path.join(os.path.dirname(__file__), '..', 'library', 'geda')] self.known_symbols = find_symbols(symbol_dirs) self.design = None self.segments = None self.net_points = None self.net_names = None self.geda_zip = None @staticmethod def auto_detect(filename): """ Return our confidence that the given file is an geda schematic """ with open(filename, 'rU') as f: data = f.read() confidence = 0 if data[0:2] == 'v ': confidence += 0.51 if 'package=' in data: confidence += 0.25 if 'footprint=' in data: confidence += 0.25 if 'refdes=' in data: confidence += 0.25 if 'netname=' in data: confidence += 0.25 return confidence def set_offset(self, point): """ Set the offset point for the gEDA output. As OpenJSON positions the origin in the center of the viewport and gEDA usually uses (40'000, 40'000) as page origin, this allows for translating from one coordinate system to another. It expects a *point* object providing a *x* and *y* attribute. """ ## create an offset of 5 grid squares from origin (0,0) self.offset.x = point.x self.offset.y = point.y def parse(self, inputfile): """ Parse a gEDA file into a design. Returns the design corresponding to the gEDA file. """ inputfiles = [] ## check if inputfile is in ZIP format if zipfile.is_zipfile(inputfile): self.geda_zip = zipfile.ZipFile(inputfile) for filename in self.geda_zip.namelist(): if filename.endswith('.sch'): inputfiles.append(filename) else: inputfiles = [inputfile] self.design = Design() ## parse frame data of first schematic to extract ## page size (assumes same frame for all files) with self._open_file_or_zip(inputfiles[0]) as stream: self._check_version(stream) for line in stream.readlines(): if 'title' in line and line.startswith('C'): obj_type, params = self._parse_command(StringIO(line)) assert (obj_type == 'C') params['basename'], _ = os.path.splitext( params['basename'], ) log.debug("using title file: %s", params['basename']) self._parse_title_frame(params) ## store offset values in design attributes self.design.design_attributes.attributes.update({ '_geda_offset_x': str(self.offset.x), '_geda_offset_y': str(self.offset.y), '_geda_frame_width': str(self.frame_width), '_geda_frame_height': str(self.frame_height), }) for filename in inputfiles: f_in = self._open_file_or_zip(filename) self._check_version(f_in) self.parse_schematic(f_in) basename, _ = os.path.splitext(os.path.basename(filename)) self.design.design_attributes.metadata.set_name(basename) ## modify offset for next page to be shifted to the right self.offset.x = self.offset.x - self.frame_width f_in.close() return self.design def _parse_v(self, stream, params): """ Only required to be callable when 'v' command is found. Returns without any processing. """ return def _parse_G(self, stream, params): """ Parse picture command 'G'. Returns without any processing but logs a warning. """ log.warn("ignoring picture/font in gEDA file. Not supported!") return def parse_schematic(self, stream): """ Parse a gEDA schematic provided as a *stream* object into a design. Returns the design corresponding to the schematic. """ # pylint: disable=R0912 if self.design is None: self.design = Design() self.segments = set() self.net_points = dict() self.net_names = dict() obj_type, params = self._parse_command(stream) while obj_type is not None: objects = getattr(self, "_parse_%s" % obj_type)(stream, params) attributes = self._parse_environment(stream) self.design.design_attributes.attributes.update(attributes or {}) self.add_objects_to_design(self.design, objects) obj_type, params = self._parse_command(stream) ## process net segments into nets & net points and add to design self.divide_segments() calculated_nets = self.calculate_nets() for cnet in sorted(calculated_nets, key=lambda n: n.net_id): self.design.add_net(cnet) return self.design def _parse_title_frame(self, params): """ Parse the frame component in *params* to extract the page size to be used in the design. The offset is adjusted according to the bottom-left position of the frame. """ ## set offset based on bottom-left corner of frame self.offset.x = params['x'] self.offset.y = params['y'] filename = self.known_symbols.get(params['basename']) if not filename or not os.path.exists(filename): log.warn("could not find title symbol '%s'" % params['basename']) self.frame_width = 46800 self.frame_height = 34000 return ## store title component name in design self.design.design_attributes.add_attribute( '_geda_titleframe', params['basename'], ) with open(filename, 'rU') as stream: obj_type, params = self._parse_command(stream) while obj_type is not None: if obj_type == 'B': if params['width'] > self.frame_width: self.frame_width = params['width'] if params['height'] > self.frame_height: self.frame_height = params['height'] ## skip commands covering multiple lines elif obj_type in ['T', 'H']: for _ in range(params['num_lines']): stream.readline() obj_type, params = self._parse_command(stream) ## set width to estimated max value when no box was found if self.frame_width == 0: self.frame_width = 46800 ## set height to estimated max value when no box was found if self.frame_height == 0: self.frame_height = 34000 def _create_ripper_segment(self, params): """ Creates a new segement from the busripper provided in gEDA. The busripper is a graphical feature that provides a nicer look for a part of a net. The bus rippers are turned into net segments according to the length and orientation in *params*. Returns a tuple of two NetPoint objects for the segment. """ x, y = params['x'], params['y'] angle, mirror = params['angle'], params['mirror'] if mirror: angle = (angle + 90) % 360 x, y = self.conv_coords(x, y) pt_a = self.get_netpoint(x, y) ripper_size = self.to_px(200) ## create second point for busripper segment on bus if angle == 0: pt_b = self.get_netpoint(pt_a.x + ripper_size, pt_a.y + ripper_size) elif angle == 90: pt_b = self.get_netpoint(pt_a.x - ripper_size, pt_a.y + ripper_size) elif angle == 180: pt_b = self.get_netpoint(pt_a.x - ripper_size, pt_a.y - ripper_size) elif angle == 270: pt_b = self.get_netpoint(pt_a.x + ripper_size, pt_a.y - ripper_size) else: raise GEDAError("invalid angle in component '%s'" % params['basename']) return pt_a, pt_b def _parse_component(self, stream, params): """ Creates a component instance according to the component *params*. If the component is not known in the library, a the component will be created according to its description in the embedded environment ``[]`` or a symbol file. The component is added to the library automatically if necessary. An instance of this component will be created and added to the design. A GEDAError is raised when either the component file is invalid or the referenced symbol file cannot be found in the known directories. Returns a tuple of Component and ComponentInstance objects. """ basename, _ = os.path.splitext(params['basename']) component_name = basename if params.get('mirror'): component_name += '_MIRRORED' if component_name in self.design.components.components: component = self.design.components.components[component_name] ## skipping embedded data might be required self.skip_embedded_section(stream) else: ##check if sym file is embedded or referenced if basename.startswith('EMBEDDED'): ## embedded only has to be processed when NOT in symbol lookup if basename not in self.known_symbols: component = self.parse_component_data(stream, params) else: if basename not in self.known_symbols: log.warn("referenced symbol file '%s' unknown" % basename) ## create a unknown symbol reference component = self.parse_component_data( StringIO(UNKNOWN_COMPONENT % basename), params) ## parse optional attached environment before continuing self._parse_environment(stream) return None, None ## requires parsing of referenced symbol file with open(self.known_symbols[basename], "rU") as f_in: self._check_version(f_in) component = self.parse_component_data(f_in, params) self.design.add_component(component_name, component) ## get all attributes assigned to component instance attributes = self._parse_environment(stream) ## refdes attribute is name of component (mandatory as of gEDA doc) ## examples if gaf repo have components without refdes, use part of ## basename if attributes is not None: instance = ComponentInstance( attributes.get('_refdes', component.name), component.name, 0) for key, value in attributes.items(): instance.add_attribute(key, value) else: instance = ComponentInstance(component.name, component.name, 0) ## generate a component instance using attributes self.design.add_component_instance(instance) symbol = SymbolAttribute(self.x_to_px(params['x']), self.y_to_px(params['y']), self.conv_angle(params['angle'], False)) instance.add_symbol_attribute(symbol) ## add annotation for special attributes for idx, attribute_key in enumerate(['_refdes', 'device']): if attribute_key in component.attributes \ or attribute_key in instance.attributes: symbol.add_annotation( Annotation('{{%s}}' % attribute_key, 0, 0 + idx * 10, 0.0, 'true')) return component, instance def _check_version(self, stream): """ Check next line in *stream* for gEDA version data starting with ``v``. Raises ``GEDAError`` when no version data can be found. """ typ, _ = self._parse_command(stream) if typ != 'v': raise GEDAError("cannot convert file, not in gEDA format") return True def _is_mirrored_command(self, params): return bool(params.get('mirror', False)) def parse_component_data(self, stream, params): """ Creates a component from the component *params* and the following commands in the stream. If the component data is embedded in the schematic file, all coordinates will be translated into the origin first. Only a single symbol/body is created for each component since gEDA symbols contain exactly one description. Returns the newly created Component object. """ # pylint: disable=R0912 basename = os.path.splitext(params['basename'])[0] saved_offset = self.offset self.offset = shape.Point(0, 0) ## retrieve if component is mirrored around Y-axis mirror = self._is_mirrored_command(params) if mirror: basename += '_MIRRORED' move_to = None if basename.startswith('EMBEDDED'): move_to = (params['x'], params['y']) ## grab next line (should be '[' typ, params = self._parse_command(stream, move_to) if typ == '[': typ, params = self._parse_command(stream, move_to) component = components.Component(basename) symbol = components.Symbol() component.add_symbol(symbol) body = components.Body() symbol.add_body(body) ##NOTE: adding this attribute to make parsing UPV data easier ## when using re-exported UPV. component.add_attribute('_geda_imported', 'true') self.pin_counter = itertools.count(0) while typ is not None: params['mirror'] = mirror objects = getattr(self, "_parse_%s" % typ)(stream, params) attributes = self._parse_environment(stream) component.attributes.update(attributes or {}) self.add_objects_to_component(component, objects) typ, params = self._parse_command(stream, move_to) self.offset = saved_offset return component def divide_segments(self): """ Checks all net segments for intersecting points of all other net segments. If an intersection is detected the net segment is divided into two segments with the intersecting point. This method has been adapted from a similar method in the kiCAD parser. """ ## check if segments need to be divided add_segs = set() rem_segs = set() for segment in self.segments: for point in self.net_points.values(): if self.intersects_segment(segment, point): pt_a, pt_b = segment rem_segs.add(segment) add_segs.add((pt_a, point)) add_segs.add((point, pt_b)) self.segments -= rem_segs self.segments |= add_segs def skip_embedded_section(self, stream): """ Reads the *stream* line by line until the end of an embedded section (``]``) is found. This method is used to skip over embedded sections of already known components. """ pos = stream.tell() typ = stream.readline().split(self.DELIMITER, 1)[0].strip() ## return with stream reset to previous position if not ## an embedded section if typ != '[': stream.seek(pos) return while typ != ']': typ = stream.readline().split(self.DELIMITER, 1)[0].strip() def get_netpoint(self, x, y): """ Creates a new NetPoint at coordinates *x*,*y* and stores it in the net point lookup table. If a NetPoint does already exist, the existing point is returned. Returns a NetPoint object at coordinates *x*,*y* """ if (x, y) not in self.net_points: self.net_points[(x, y)] = net.NetPoint('%da%d' % (x, y), x, y) return self.net_points[(x, y)] @staticmethod def intersects_segment(segment, pt_c): """ Checks if point *pt_c* lays on the *segment*. This code is adapted from the kiCAD parser. Returns True if *pt_c* is on *segment*, False otherwise. """ pt_a, pt_b = segment #check vertical segment if pt_a.x == pt_b.x == pt_c.x: if min(pt_a.y, pt_b.y) < pt_c.y < max(pt_a.y, pt_b.y): return True #check vertical segment elif pt_a.y == pt_b.y == pt_c.y: if min(pt_a.x, pt_b.x) < pt_c.x < max(pt_a.x, pt_b.x): return True #check diagonal segment elif (pt_c.x-pt_a.x)*(pt_b.y-pt_a.y) \ == (pt_b.x-pt_a.x)*(pt_c.y-pt_a.y): if min(pt_a.x, pt_b.x) < pt_c.x < max(pt_a.x, pt_b.x): return True ## point C not on segment return False def _parse_environment(self, stream): """ Checks if attribute environment starts in the next line (marked by '{'). Environment only contains text elements interpreted as text. Returns a dictionary of attributes. """ current_pos = stream.tell() typ, params = self._parse_command(stream) #go back to previous position when no environment in stream if typ != '{': stream.seek(current_pos) return None typ, params = self._parse_command(stream) attributes = {} while typ is not None: if typ == 'T': geda_text = self._parse_T(stream, params) if geda_text.is_attribute(): attributes[geda_text.attribute] = geda_text.content else: log.warn( "normal text in environemnt does not comply " "with GEDA format specification: %s", geda_text.content) typ, params = self._parse_command(stream) return attributes def calculate_nets(self): """ Calculate connected nets from previously stored segments and netpoints. The code has been adapted from the kiCAD parser since the definition of segments in the schematic file are similar. The segments are checked against existing nets and added when they touch it. For this to work, it is required that intersecting segments are divided prior to this method. Returns a list of valid nets and its net points. """ nets = [] # Iterate over the segments, removing segments when added to a net while self.segments: seg = self.segments.pop() # pick a point net_name = '' pt_a, pt_b = seg if pt_a.point_id in self.net_names: net_name = self.net_names[pt_a.point_id] elif pt_b.point_id in self.net_names: net_name = self.net_names[pt_b.point_id] new_net = net.Net(net_name) new_net.connect(seg) found = True if net_name: new_net.attributes['_name'] = net_name while found: found = set() for seg in self.segments: # iterate over segments if new_net.connected(seg): # segment touching the net new_net.connect(seg) # add the segment found.add(seg) for seg in found: self.segments.remove(seg) nets.append(new_net) # check if names are available for calculated nets for net_obj in nets: for point_id in net_obj.points: ## check for stored net names based on pointIDs if point_id in self.net_names: net_obj.net_id = self.net_names[point_id] net_obj.attributes['_name'] = self.net_names[point_id] if '_name' in net_obj.attributes: annotation = Annotation( "{{_name}}", ## annotation referencing attribute '_name' 0, 0, self.conv_angle(0.0), self.conv_bool(1), ) net_obj.add_annotation(annotation) for net_obj in nets: if not net_obj.net_id: net_obj.net_id = min(net_obj.points) return nets def _open_file_or_zip(self, filename, mode='rU'): """ Open the file with *filename* and return a file handle for it. If the current file is a ZIP file the filename will be treated as compressed file in this ZIP file. """ if self.geda_zip is not None: temp_dir = tempfile.mkdtemp() self.geda_zip.extract(filename, temp_dir) filename = os.path.join(temp_dir, filename) return open(filename, mode) def add_text_to_component(self, component, geda_text): """ Add the content of a ``GEDAText`` instance to the component. If *geda_text* contains ``refdes``, ``prefix`` or ``suffix`` attributes it will be stored as special attribute in the component. *geda_text* that is not an attribute will be added as ``Label`` to the components body. """ if geda_text.is_text(): component.symbols[0].bodies[0].add_shape(geda_text.as_label()) elif geda_text.attribute == '_refdes' \ and '?' in geda_text.content: prefix, suffix = geda_text.content.split('?') component.add_attribute('_prefix', prefix) component.add_attribute('_suffix', suffix) else: component.add_attribute(geda_text.attribute, geda_text.content) def add_objects_to_component(self, component, objs): """ Add a GEDA object to the component. Valid objects are subclasses of ``Shape``, ``Pin`` or ``GEDAText``. *objs* is expected to be an iterable and will be added to the correct component properties according to their type. """ if not objs: return try: iter(objs) except TypeError: objs = [objs] for obj in objs: obj_cls = obj.__class__ if issubclass(obj_cls, shape.Shape): component.symbols[0].bodies[0].add_shape(obj) elif issubclass(obj_cls, components.Pin): component.symbols[0].bodies[0].add_pin(obj) elif issubclass(obj_cls, GEDAText): self.add_text_to_component(component, obj) def add_text_to_design(self, design, geda_text): """ Add the content of a ``GEDAText`` instance to the design. If *geda_text* contains ``use_license`` it will be added to the design's metadata ``license`` other attributes are added to ``design_attributes``. *geda_text* that is not an attribute will be added as ``Label`` to the components body. """ if geda_text.is_text(): design.add_shape(geda_text.as_label()) elif geda_text.attribute == 'use_license': metadata = design.design_attributes.metadata metadata.license = geda_text.content else: design.design_attributes.add_attribute( geda_text.attribute, geda_text.content, ) def add_objects_to_design(self, design, objs): """ Add a GEDA object to the design. Valid objects are subclasses of ``Shape``, ``Pin`` or ``GEDAText``. *objs* is expected to be an iterable and will be added to the correct component properties according to their type. """ if not objs: return try: iter(objs) except TypeError: objs = [objs] for obj in objs: obj_cls = obj.__class__ if issubclass(obj_cls, shape.Shape): design.add_shape(obj) elif issubclass(obj_cls, components.Pin): design.add_pin(obj) elif issubclass(obj_cls, GEDAText): self.add_text_to_design(design, obj) def _parse_U(self, stream, params): """ Processing a bus instance with start end end coordinates at (x1, y1) and (x2, y2). *color* is ignored. *ripperdir* defines the direction in which the bus rippers are oriented relative to the direction of the bus. """ x1, x2 = params['x1'], params['x2'] y1, y2 = params['y1'], params['y2'] ## ignore bus when length is zero if x1 == x2 and y1 == y2: return pta_x, pta_y = self.conv_coords(x1, y1) ptb_x, ptb_y = self.conv_coords(x2, y2) self.segments.add( (self.get_netpoint(pta_x, pta_y), self.get_netpoint(ptb_x, ptb_y))) def _parse_L(self, stream, params): """ Creates a Line object from the parameters in *params*. All style related parameters are ignored. Returns a Line object. """ line_x1 = params['x1'] line_x2 = params['x2'] if self._is_mirrored_command(params): line_x1 = 0 - params['x1'] line_x2 = 0 - params['x2'] line = shape.Line( self.conv_coords(line_x1, params['y1']), self.conv_coords(line_x2, params['y2']), ) ## store style data for line in 'style' dict self._save_parameters_to_object(line, params) return line def _parse_B(self, stream, params): """ Creates rectangle from gEDA box with origin in bottom left corner. All style related values are ignored. Returns a Rectangle object. """ rect_x = params['x'] if self._is_mirrored_command(params): rect_x = 0 - (rect_x + params['width']) rect = shape.Rectangle(self.x_to_px(rect_x), self.y_to_px(params['y'] + params['height']), self.to_px(params['width']), self.to_px(params['height'])) ## store style data for rect in 'style' dict self._save_parameters_to_object(rect, params) return rect def _parse_V(self, stream, params): """ Creates a Circle object from the gEDA parameters in *params. All style related parameters are ignored. Returns a Circle object. """ vertex_x = params['x'] if self._is_mirrored_command(params): vertex_x = 0 - vertex_x circle = shape.Circle( self.x_to_px(vertex_x), self.y_to_px(params['y']), self.to_px(params['radius']), ) ## store style data for arc in 'style' dict self._save_parameters_to_object(circle, params) return circle def _parse_A(self, stream, params): """ Creates an Arc object from the parameter in *params*. All style related parameters are ignored. Returns Arc object. """ arc_x = params['x'] start_angle = params['startangle'] sweep_angle = params['sweepangle'] if self._is_mirrored_command(params): arc_x = 0 - arc_x start_angle = start_angle + sweep_angle if start_angle <= 180: start_angle = 180 - start_angle else: start_angle = (360 - start_angle) + 180 arc = shape.Arc( self.x_to_px(arc_x), self.y_to_px(params['y']), self.conv_angle(start_angle), self.conv_angle(start_angle + sweep_angle), self.to_px(params['radius']), ) ## store style data for arc in 'style' dict self._save_parameters_to_object(arc, params) return arc def _parse_T(self, stream, params): """ Parses text element and determins if text is a text object or an attribute. Returns a tuple (key, value). If text is an annotation key is None. """ params['x'] = self.x_to_px(params['x']) params['y'] = self.y_to_px(params['y']) params['angle'] = self.conv_angle(params['angle']) geda_text = GEDAText.from_command(stream, params) ## text can have environemnt attached: parse & ignore self._parse_environment(stream) return geda_text def _parse_N(self, stream, params): """ Creates a segment from the command *params* and stores it in the global segment list for further processing in :py:method:divide_segments and :py:method:calculate_nets. It also extracts the net name from the attribute environment if present. """ ## store segement for processing later x1, y1 = self.conv_coords(params['x1'], params['y1']) x2, y2 = self.conv_coords(params['x2'], params['y2']) ## store segment points in global point list pt_a = self.get_netpoint(x1, y1) pt_b = self.get_netpoint(x2, y2) ## add segment to global list for later processing self.segments.add((pt_a, pt_b)) attributes = self._parse_environment(stream) if attributes is not None: ## create net with name in attributes if '_netname' in attributes: net_name = attributes['_netname'] if net_name not in self.net_names.values(): self.net_names[pt_a.point_id] = net_name def _parse_P(self, stream, params, pinnumber=0): """ Creates a Pin object from the parameters in *param* and text attributes provided in the following environment. The environment is enclosed in ``{}`` and is required. If no attributes can be extracted form *stream* an GEDAError is raised. The *pin_id* is retrieved from the 'pinnumber' attribute and all other attributes are ignored. The conneted end of the pin is taken from the 'whichend' parameter as defined in the gEDA documentation. Returns a Pin object. """ ## pin requires an attribute enviroment, so parse it first attributes = self._parse_environment(stream) if attributes is None: log.warn('mandatory pin attributes missing') attributes = { '_pinnumber': pinnumber, } if '_pinnumber' not in attributes: attributes['_pinnumber'] = pinnumber log.warn("mandatory attribute '_pinnumber' not assigned to pin") whichend = params['whichend'] pin_x1, pin_x2 = params['x1'], params['x2'] if self._is_mirrored_command(params): pin_x1 = 0 - pin_x1 pin_x2 = 0 - pin_x2 ## determine wich end of the pin is the connected end ## 0: first point is connector ## 1: second point is connector if whichend == 0: connect_end = self.conv_coords(pin_x1, params['y1']) null_end = self.conv_coords(pin_x2, params['y2']) else: null_end = self.conv_coords(pin_x1, params['y1']) connect_end = self.conv_coords(pin_x2, params['y2']) label = None if '_pinlabel' in attributes: label = shape.Label(connect_end[0], connect_end[1], attributes.get('_pinlabel'), 'left', 0.0) pin = components.Pin( attributes['_pinnumber'], #pin number null_end, connect_end, label=label) ## store style parameters in shape's style dict self._save_parameters_to_object(pin, params) return pin def _parse_C(self, stream, params): """ Parse component command 'C'. *stream* is the file stream pointing to the line after the component command. *params* are the parsed parameters from the component command. The method checks if component is a title and ignores it if that is the case due to previous processing. If the component is a busripper, it is converted into a net segment. Otherwise, the component is parsed as a regular component and added to the library and design. """ ## ignore title since it only defines the blueprint frame if params['basename'].startswith('title'): self._parse_environment(stream) ## busripper are virtual components that need separate ## processing elif 'busripper' in params['basename']: self.segments.add(self._create_ripper_segment(params)) ## make sure following environments are ignored self.skip_embedded_section(stream) self._parse_environment(stream) else: self._parse_component(stream, params) def _parse_H(self, stream, params): """ Parses a SVG-like path provided path into a list of simple shapes. The gEDA formats allows only line and curve segments with absolute coordinates. Hence, shapes are either Line or BezierCurve objects. The method processes the stream data according to the number of lines in *params*. Returns a list of Line and BezierCurve shapes. """ params['extra_id'] = self.path_counter.next() num_lines = params['num_lines'] mirrored = self._is_mirrored_command(params) command = stream.readline().strip().split(self.DELIMITER) if command[0] != 'M': raise GEDAError('found invalid path in gEDA file') def get_coords(string, mirrored): """ Get coordinates from string with comma-sparated notation.""" x, y = [int(value) for value in string.strip().split(',')] if mirrored: x = -x return (self.x_to_px(x), self.y_to_px(y)) shapes = [] current_pos = initial_pos = (get_coords(command[1], mirrored)) ## loop over the remaining lines of commands (after 'M') for _ in range(num_lines - 1): command = stream.readline().strip().split(self.DELIMITER) ## draw line from current to given position if command[0] == 'L': assert (len(command) == 2) end_pos = get_coords(command[1], mirrored) shape_ = shape.Line(current_pos, end_pos) current_pos = end_pos ## draw curve from current to given position elif command[0] == 'C': assert (len(command) == 4) control1 = get_coords(command[1], mirrored) control2 = get_coords(command[2], mirrored) end_pos = get_coords(command[3], mirrored) shape_ = shape.BezierCurve(control1, control2, current_pos, end_pos) current_pos = end_pos ## end of sub-path, straight line from current to initial position elif command[0] in ['z', 'Z']: shape_ = shape.Line(current_pos, initial_pos) else: raise GEDAError("invalid command type in path '%s'" % command[0]) ## store style parameters in shape's style dict self._save_parameters_to_object(shape_, params) shapes.append(shape_) return shapes def _save_parameters_to_object(self, obj, params): """ Save all ``style`` and ``extra`` parameters to the objects ``styles`` dictionary. If *obj* does not have a ``styles`` property, a ``GEDAError`` is raised. """ parameter_types = [ geda_commands.GEDAStyleParameter.TYPE, geda_commands.GEDAExtraParameter.TYPE, ] try: for key, value in params.items(): if key.split('_')[0] in parameter_types: obj.styles[key] = value except AttributeError: log.exception( "tried saving style data to '%s' without styles dict.", obj.__class__.__name__) def _parse_command(self, stream, move_to=None): """ Parse the next command in *stream*. The object type is check for validity and its parameters are parsed and converted to the expected typs in the parsers lookup table. If *move_to* is provided it is used to translate all coordinates into by the given coordinate. Returns a tuple (*object type*, *parameters*) where *parameters* is a dictionary of paramter name and value. Raises GEDAError when object type is not known. """ line = stream.readline() while line.startswith('#') or line == '\n': line = stream.readline() command_data = line.strip().split(self.DELIMITER) if len(command_data[0]) == 0 or command_data[0] in [']', '}']: return None, [] object_type, command_data = command_data[0].strip(), command_data[1:] if object_type not in self.OBJECT_TYPES: raise GEDAError("unknown type '%s' in file" % object_type) params = {} geda_command = self.OBJECT_TYPES[object_type] for idx, parameter in enumerate(geda_command.parameters()): if idx >= len(command_data): ## prevent text commands of version 1 from breaking params[parameter.name] = parameter.default else: datatype = parameter.datatype params[parameter.name] = datatype(command_data[idx]) assert (len(params) == len(geda_command.parameters())) if move_to is not None: ## element in EMBEDDED component need to be moved ## to origin (0, 0) from component origin if object_type in ['T', 'B', 'C', 'A']: params['x'] = params['x'] - move_to[0] params['y'] = params['y'] - move_to[1] elif object_type in ['L', 'P']: params['x1'] = params['x1'] - move_to[0] params['y1'] = params['y1'] - move_to[1] params['x2'] = params['x2'] - move_to[0] params['y2'] = params['y2'] - move_to[1] return object_type, params @classmethod def to_px(cls, value): """ Converts value in MILS to pixels using the parsers scale factor. Returns an integer value converted to pixels. """ return int(value / cls.SCALE_FACTOR) def x_to_px(self, x_mils): """ Convert *px* from MILS to pixels using the scale factor and translating it allong the X-axis in offset. Returns translated and converted X coordinate. """ return int(float(x_mils - self.offset.x) / self.SCALE_FACTOR) def y_to_px(self, y_mils): """ Convert *py* from MILS to pixels using the scale factor and translating it allong the Y-axis in offset. Returns translated and converted Y coordinate. """ return int(float(y_mils - self.offset.y) / self.SCALE_FACTOR) def conv_coords(self, orig_x, orig_y): """ Converts coordinats *orig_x* and *orig_y* from MILS to pixel units based on scale factor. The converted coordinates are in multiples of 10px. """ orig_x, orig_y = int(orig_x), int(orig_y) return (self.x_to_px(orig_x), self.y_to_px(orig_y)) @staticmethod def conv_bool(value): """ Converts *value* into string representing boolean 'true' or 'false'. *value* can be of any numeric or boolean type. """ if value in ['true', 'false']: return value return str(bool(int(value)) is True).lower() @staticmethod def conv_angle(angle): """ Converts *angle* (in degrees) to pi radians. gEDA sets degree angles counter-clockwise whereas upverter uses pi radians clockwise. Therefore the direction of *angle* is therefore adjusted first. """ angle = angle % 360.0 if angle > 0: angle = abs(360 - angle) return round(angle / 180.0, 1)
class JSON: """ The Open JSON Format Parser This is mostly for sanity checks, it reads in the Open JSON format, and then outputs it. """ def __init__(self): self.design = Design() @staticmethod def auto_detect(filename): """ Return our confidence that the given file is an openjson file """ with open(filename, 'r') as f: data = f.read() confidence = 0 if 'component_instances' in data: confidence += 0.3 if 'design_attributes' in data: confidence += 0.6 return confidence def parse(self, filename): """ Parse the openjson file into the core. """ with open(filename) as f: read = json.loads(f.read()) self.parse_component_instances(read.get('component_instances')) self.parse_components(read.get('components')) if read.get('shapes') is not None: self.parse_sch_shapes(read.get('shapes')) self.parse_design_attributes(read.get('design_attributes')) self.parse_nets(read.get('nets')) self.parse_version(read.get('version')) return self.design def parse_version(self, version): """ Extract the file version. """ file_version = version.get('file_version') exporter = version.get('exporter') self.design.set_version(file_version, exporter) def parse_component_instances(self, component_instances): """ Extract the component instances. """ for instance in component_instances: # Get instance_id, library_id and symbol_index instance_id = instance.get('instance_id') library_id = instance.get('library_id') symbol_index = int(instance.get('symbol_index')) # Make the ComponentInstance() inst = ComponentInstance(instance_id, library_id, symbol_index) # Get the SymbolAttributes for symbol_attribute in instance.get('symbol_attributes'): attr = self.parse_symbol_attribute(symbol_attribute) inst.add_symbol_attribute(attr) # Get the Attributes for key, value in instance.get('attributes').items(): inst.add_attribute(key, value) # Add the ComponentInstance self.design.add_component_instance(inst) def parse_symbol_attribute(self, symbol_attribute): """ Extract attributes from a symbol. """ x = int(symbol_attribute.get('x') or 0) y = int(symbol_attribute.get('y') or 0) rotation = float(symbol_attribute.get('rotation')) # Make SymbolAttribute symbol_attr = SymbolAttribute(x, y, rotation) # Add Annotations for annotation in symbol_attribute.get('annotations'): anno = self.parse_annotation(annotation) symbol_attr.add_annotation(anno) # Return SymbolAttribute to be added to it's ComponentInstance return symbol_attr def parse_annotation(self, annotation): """ Extract an annotation. """ value = annotation.get('value') x = int(annotation.get('x')) y = int(annotation.get('y')) rotation = float(annotation.get('rotation')) visible = annotation.get('visible') if visible is not None and visible.lower() == 'false': visible = 'false' else: visible = 'true' return Annotation(value, x, y, rotation, visible) def parse_components(self, components): """ Extract a component library. """ for library_id, component in components.items(): name = component.get('name') comp = Component(name) # Get attributes for key, value in component.get('attributes').items(): comp.add_attribute(key, value) for symbol in component.get('symbols'): symb = self.parse_symbol(symbol) comp.add_symbol(symb) self.design.add_component(library_id, comp) def parse_sch_shapes(self, shapes): """ Extract shapes drawn directly on the schematic. """ for sh in shapes: self.design.add_shape(self.parse_shape(sh)) def parse_symbol(self, symbol): """ Extract a symbol. """ symb = Symbol() for body in symbol.get('bodies'): bdy = self.parse_body(body) symb.add_body(bdy) return symb def parse_body(self, body): """ Extract a body of a symbol. """ bdy = Body() for pin in body.get('pins'): parsed_pin = self.parse_pin(pin) bdy.add_pin(parsed_pin) for shape in body.get('shapes'): parsed_shape = self.parse_shape(shape) bdy.add_shape(parsed_shape) return bdy def parse_pin(self, pin): """ Extract a pin of a body. """ pin_number = pin.get('pin_number') p1 = self.parse_point(pin.get('p1')) p2 = self.parse_point(pin.get('p2')) parsed_pin = Pin(pin_number, p1, p2) if pin.get('label') is not None: parsed_pin.label = self.parse_label(pin.get('label')) parsed_pin.styles = pin.get('styles') or {} return parsed_pin def parse_point(self, point): """ Extract a point. """ x = int(point.get('x')) y = int(point.get('y')) return Point(x, y) def parse_label(self, label): """ Extract a label. """ x = int(label.get('x')) y = int(label.get('y')) text = label.get('text') align = label.get('align') rotation = float(label.get('rotation')) parsed_label = Label(x, y, text, align, rotation) parsed_label.styles = label.get('styles') or {} return parsed_label def parse_shape(self, shape): """ Extract a shape. """ # pylint: disable=R0914 # pylint: disable=R0911 typ = shape.get('type') if 'rectangle' == typ: x = int(shape.get('x')) y = int(shape.get('y')) height = int(shape.get('height')) width = int(shape.get('width')) parsed_shape = Rectangle(x, y, width, height) elif 'rounded_rectangle' == typ: x = int(shape.get('x')) y = int(shape.get('y')) height = int(shape.get('height')) width = int(shape.get('width')) radius = int(shape.get('radius')) parsed_shape = RoundedRectangle(x, y, width, height, radius) elif 'arc' == typ: x = int(shape.get('x')) y = int(shape.get('y')) start_angle = float(shape.get('start_angle')) end_angle = float(shape.get('end_angle')) radius = int(shape.get('radius')) parsed_shape = Arc(x, y, start_angle, end_angle, radius) elif 'circle' == typ: x = int(shape.get('x')) y = int(shape.get('y')) radius = int(shape.get('radius')) parsed_shape = Circle(x, y, radius) elif 'label' == typ: x = int(shape.get('x')) y = int(shape.get('y')) rotation = float(shape.get('rotation')) text = shape.get('text') align = shape.get('align') parsed_shape = Label(x, y, text, align, rotation) elif 'line' == typ: p1 = self.parse_point(shape.get('p1')) p2 = self.parse_point(shape.get('p2')) parsed_shape = Line(p1, p2) elif 'polygon' == typ: parsed_shape = Polygon() for point in shape.get('points'): parsed_shape.add_point(self.parse_point(point)) elif 'bezier' == typ: control1 = self.parse_point(shape.get('control1')) control2 = self.parse_point(shape.get('control2')) p1 = self.parse_point(shape.get('p1')) p2 = self.parse_point(shape.get('p2')) parsed_shape = BezierCurve(control1, control2, p1, p2) parsed_shape.styles = shape.get('styles') or {} parsed_shape.attributes = shape.get('attributes') or {} return parsed_shape def parse_design_attributes(self, design_attributes): """ Extract design attributes. """ attrs = DesignAttributes() # Get the Annotations for annotation in design_attributes.get('annotations'): anno = self.parse_annotation(annotation) attrs.add_annotation(anno) # Get the Attributes for key, value in design_attributes.get('attributes').items(): attrs.add_attribute(key, value) # Get the Metadata meta = self.parse_metadata(design_attributes.get('metadata')) attrs.set_metadata(meta) self.design.set_design_attributes(attrs) def parse_metadata(self, metadata): """ Extract design meta-data. """ meta = Metadata() meta.set_name(metadata.get('name')) meta.set_license(metadata.get('license')) meta.set_owner(metadata.get('owner')) meta.set_updated_timestamp(metadata.get('updated_timestamp')) meta.set_design_id(metadata.get('design_id')) meta.set_description(metadata.get('description')) meta.set_slug(metadata.get('slug')) for attached_url in metadata.get('attached_urls'): meta.add_attached_url(attached_url) return meta def parse_nets(self, nets): """ Extract nets. """ for net in nets: net_id = net.get('net_id') ret_net = Net(net_id) # Add Annotations for annotation in net.get('annotations'): anno = self.parse_annotation(annotation) ret_net.add_annotation(anno) # Get the Attributes for key, value in net.get('attributes').items(): ret_net.add_attribute(key, value) # Get the Points for net_point in net.get('points'): npnt = self.parse_net_point(net_point) ret_net.add_point(npnt) self.design.add_net(ret_net) def parse_net_point(self, net_point): """ Extract a net point. """ point_id = net_point.get('point_id') x = int(net_point.get('x')) y = int(net_point.get('y')) npnt = NetPoint(point_id, x, y) # Get the connected points for point in net_point.get('connected_points'): npnt.add_connected_point(point) # Get the ConnectedComponents for connectedcomponent in net_point.get('connected_components'): conn_comp = self.parse_connected_component(connectedcomponent) npnt.add_connected_component(conn_comp) return npnt def parse_connected_component(self, connectedcomponent): """ Extract a connected component. """ instance_id = connectedcomponent.get('instance_id') pin_number = connectedcomponent.get('pin_number') return ConnectedComponent(instance_id, pin_number)
def parse(self, inputfile): """ Parse a gEDA file into a design. Returns the design corresponding to the gEDA file. """ inputfiles = [] ## check if inputfile is in ZIP format if zipfile.is_zipfile(inputfile): self.geda_zip = zipfile.ZipFile(inputfile) for filename in self.geda_zip.namelist(): if filename.endswith('.sch'): inputfiles.append(filename) else: inputfiles = [inputfile] self.design = Design() ## parse frame data of first schematic to extract ## page size (assumes same frame for all files) with self._open_file_or_zip(inputfiles[0]) as stream: self._check_version(stream) for line in stream.readlines(): if 'title' in line and line.startswith('C'): obj_type, params = self._parse_command(StringIO(line)) assert (obj_type == 'C') params['basename'], _ = os.path.splitext( params['basename'], ) log.debug("using title file: %s", params['basename']) self._parse_title_frame(params) ## store offset values in design attributes self.design.design_attributes.attributes.update({ '_geda_offset_x': str(self.offset.x), '_geda_offset_y': str(self.offset.y), '_geda_frame_width': str(self.frame_width), '_geda_frame_height': str(self.frame_height), }) for filename in inputfiles: f_in = self._open_file_or_zip(filename) self._check_version(f_in) self.parse_schematic(f_in) basename, _ = os.path.splitext(os.path.basename(filename)) self.design.design_attributes.metadata.set_name(basename) ## modify offset for next page to be shifted to the right self.offset.x = self.offset.x - self.frame_width f_in.close() return self.design
def __init__(self): self.design = Design()
class EagleXML(object): """ The Eagle XML Format Parser. This parser uses code generated by generateDS.py which converts an xsd file to a set of python objects with parse and export functions. That code is in generated.py. It was created by the following steps: 1. Started with eagle.dtd from Eagle 6.2.0. 2. Removed inline comments in dtd (was breaking conversion to xsd). The dtd is also stored in this directory. 3. Converted to eagle.xsd using dtd2xsd.pl from w3c. The xsd is also stored in this directory. 4. Run a modified version of generateDS.py with the following arguments: --silence --external-encoding=utf-8 -o generated.py """ MULT = 90 / 25.4 # mm to 90 dpi def __init__(self): self.design = Design() # map components to gate names to symbol indices self.cpt2gate2symbol_index = defaultdict(dict) @staticmethod def auto_detect(filename): """ Return our confidence that the given file is an eagle xml schematic """ with open(filename, 'r') as f: data = f.read(4096) confidence = 0.0 if 'eagle.dtd' in data: confidence += 0.9 return confidence def parse(self, filename): """ Parse an Eagle XML file into a design """ root = parse(filename) self.make_components(root) self.make_component_instances(root) return self.design def make_components(self, root): """ Construct openjson components for an eagle model. """ for lib in get_subattr(root, 'drawing.schematic.libraries.library', ()): for deviceset in get_subattr(lib, 'devicesets.deviceset', ()): cpt = self.make_component(lib, deviceset) self.design.components.add_component(cpt.name, cpt) def make_component(self, lib, deviceset): """ Construct an openjson component for a deviceset in a library. """ cpt = Component(lib.name + ':' + deviceset.name) for gate in get_subattr(deviceset, 'gates.gate'): symbol = Symbol() cpt.add_symbol(symbol) self.cpt2gate2symbol_index[cpt][gate.name] = len(cpt.symbols) - 1 symbol.add_body(self.make_body_from_symbol(lib, gate.symbol)) return cpt def make_body_from_symbol(self, lib, symbol_name): """ Contruct an openjson Body from an eagle symbol in a library. """ body = Body() symbol = [ s for s in get_subattr(lib, 'symbols.symbol') if s.name == symbol_name ][0] for wire in symbol.wire: body.add_shape( Line((self.make_length(wire.x1), self.make_length(wire.y1)), (self.make_length(wire.x2), self.make_length(wire.y2)))) return body def make_component_instances(self, root): """ Construct openjson component instances for an eagle model. """ parts = dict( (p.name, p) for p in get_subattr(root, 'drawing.schematic.parts.part', ())) for sheet in get_subattr(root, 'drawing.schematic.sheets.sheet', ()): for instance in get_subattr(sheet, 'instances.instance', ()): inst = self.make_component_instance(parts, instance) self.design.add_component_instance(inst) def make_component_instance(self, parts, instance): """ Construct an openjson component instance for an eagle instance. """ part = parts[instance.part] library_id = part.library + ':' + part.deviceset # TODO pick correct symbol index inst = ComponentInstance(instance.part, library_id, 0) # TODO handle mirror # TODO handle smashed? attr = SymbolAttribute(self.make_length(instance.x), self.make_length(instance.y), self.make_angle(instance.rot or '0')) inst.add_symbol_attribute(attr) return inst def make_length(self, value): """ Make an openjson length measurement from an eagle length. """ return int(round(float(value) * self.MULT)) def make_angle(self, value): """ Make an openjson angle measurement from an eagle angle. """ return float(value.lstrip('MSR')) / 180