From 67013ae17af0436b930dce450a813239be969601 Mon Sep 17 00:00:00 2001 From: Trygve Laugstøl Date: Tue, 27 Dec 2016 01:14:37 +0100 Subject: o Initial import of some tools for working with KiCAD BOM files and Digi-key. --- trygvis/eda/cli/kicad_bom_to_ttl.py | 356 ++++++++++++++++++++++++++++++++++++ 1 file changed, 356 insertions(+) create mode 100755 trygvis/eda/cli/kicad_bom_to_ttl.py (limited to 'trygvis/eda/cli/kicad_bom_to_ttl.py') diff --git a/trygvis/eda/cli/kicad_bom_to_ttl.py b/trygvis/eda/cli/kicad_bom_to_ttl.py new file mode 100755 index 0000000..c7d6899 --- /dev/null +++ b/trygvis/eda/cli/kicad_bom_to_ttl.py @@ -0,0 +1,356 @@ +#!/usr/bin/env python3 + +import functools +import os.path +import re +import xml.etree.ElementTree +from urllib.parse import quote_plus + +from trygvis.eda import cli +from trygvis.eda.kicad import rdf as kicad_rdf + + +def _clean(s): + if s is None: + return None + s = s.strip() + return None if len(s) == 0 else s + + +def _cleaned_text(e, name): + child = e.find(name) + if child is None: + return None + return child.text + + +def _cleaned_attr(e, name): + value = e.get(name) + if value is None: + return None + return _clean(value) + + +def _iso_size_to_factor(size): + if size == 'k': + return 3 + if size == 'M': + return 6 + if size == 'm': + return -3 + if size == 'u' or size == '\N{GREEK SMALL LETTER MU}' or size == '\N{MICRO SIGN}': + return -6 + if size == 'n': + return -9 + if size == 'p': + return -12 + return None + + +@functools.total_ordering +class Value: + valueRe = re.compile("([0-9]+\.?[0-9]*)(.*)") + typeRe = re.compile("(.*)(H|Hz)$") + + def __init__(self, text, value, factor, type): + self.text = text + self.value = value + self.factor = factor + self.type = type + + def __eq__(self, other): + return self.text == other.text + + def __lt__(self, other): + if self.value is not None: + if other.value is None: + return 1 + else: + return (self.factor, self.value) < (other.factor, other.value) + else: + if other.value is None: + return self.text < other.text + else: + return -1 + + def __str__(self): + if self.value is not None: + s = str(self.value) + 'e' + str(self.factor) + if self.type is not None: + s = s + ' ' + self.type + return s + if self.text is not None: + return self.text + return "unknown" + + @staticmethod + def parse(text): + if text is None: + return Value(None, None, None, None) + + m = Value.valueRe.match(text) + if m: + value_int = float(m.group(1)) + suffix = m.group(2) + m = Value.typeRe.match(suffix) + if m: + suffix = m.group(1) + type = m.group(2) + else: + type = None + + factor = _iso_size_to_factor(suffix) + # cli.info(text + ': suffix:' + suffix + ' => value_int: ' + str(value_int) + ', factor: ' + str( + # factor) + ', type=' + str(type)) + + if value_int is not None and factor is not None: + return Value(text, value_int, factor, type) + + return Value(text, None, None, None) + + +class Component: + refRe = re.compile("([a-zA-Z]+)([0-9]+)") + + def __init__(self, ref, value, footprint, fields): + self.ref = ref + self.value = Value.parse(value) + self.footprint = footprint + self.fields = fields + m = Component.refRe.match(ref) + if m: + self.ref_class = m.group(1) + self.ref_instance = int(m.group(2)) + else: + self.ref_class = None + self.ref_instance = None + + def __str__(self): + s = 'ref=' + self.ref + if self.value is not None: + s += ', value=' + str(self.value) + if self.footprint is not None: + s += ', footprint=' + self.footprint + for name, value in self.fields.items(): + s += ', ' + name + '=' + value + return s + + def find_field(self, key): + return self.fields[key] if key in self.fields else None + + @staticmethod + def from_xml(e): + ref = e.get('ref') + value = _cleaned_text(e, 'value') + footprint = _cleaned_text(e, 'footprint') + fs = {} + + fields = e.find('fields') + if fields is not None: + for field in fields.findall('field'): + fs[field.get('name')] = field.text + + if ref is not None: + return Component(ref, value, footprint, fs) + + return None + + +class TitleBlock(object): + def __init__(self, title=None, rev=None, date=None, source=None): + self.title = title + self.rev = rev + self.date = date + self.source = source + + @staticmethod + def from_xml(tb): + title = _cleaned_text(tb, 'title') + rev = _cleaned_text(tb, 'rev') + date = _cleaned_text(tb, 'date') + source = _cleaned_text(tb, 'source') + return TitleBlock(title, rev, date, source) + + +class Sheet(object): + def __init__(self, number, name, title_block): + self.number = number + self.name = name + self.title_block = title_block + + @staticmethod + def from_xml(s): + number = _cleaned_attr(s, "number") + name = _cleaned_attr(s, "name") + node = s.find('title_block') + title_block = TitleBlock.from_xml(node) if node is not None else TitleBlock() + return Sheet(int(number) if number is not None else None, name, title_block) + + +class Design(object): + def __init__(self, source=None, date=None, tool=None, sheets=None): + self.source = source + self.date = date + self.tool = tool + self.sheets = sheets if sheets is not None else [] + + @staticmethod + def from_xml(d): + source = _cleaned_text(d, 'source') + date = _cleaned_text(d, 'date') + tool = _cleaned_text(d, 'tool') + sheets = [] + for s in d.iterfind('sheet'): + sheets.append(Sheet.from_xml(s)) + return Design(source, date, tool, sheets) + + +class Export(object): + def __init__(self, design, components): + self.design = design if design is not None else Design() + self.components = components + + @staticmethod + def from_xml(doc): + cs = [] + + node = doc.find('design') + design = Design.from_xml(node) if node is not None else None + + components = doc.find('components') + for e in components.iter('comp'): + c = Component.from_xml(e) + if c is not None: + cs.append(c) + + return Export(design, cs) + + def component_fields(self): + fs = set() + for c in self.components: + for key, value in c.fields.items(): + fs.add(key) + return fs + + def generate_schematic_url(self): + s = next((s for s in self.design.sheets if s.number == 1), None) + title = s.title_block.title if s is not None else None + if title is None: + if self.design.source is not None: + title = _clean(os.path.basename(self.design.source)) + + if title is None: + raise cli.CliException("Could not generate a stable, identifying URL for the schematic") + + title = quote_plus(title) + return kicad_rdf.KICAD_BOARD[title] + + +def run(src, dst, args): + from trygvis.eda import cli + + tree = xml.etree.ElementTree.parse(src) + root = tree.getroot() + + export = Export.from_xml(root) + + # print("components:") + # for c in export.components: + # print(c) + + # for name in export.component_fields(): + # cli.info(name) + + from operator import attrgetter + import itertools + + # cli.info("components:") + # fmt = '%2s%3s' + # cli.info(fmt % ('Ref', '')) + + components = sorted(export.components, key=attrgetter('ref')) + + component_count = 0 + part_count = 0 + has_part = 0 + has_digikey = 0 + + for cls, cs in itertools.groupby(components, key=attrgetter('ref_class')): + # cli.info('--- Class: %s ---' % cls) + cs = sorted(cs, key=attrgetter('value')) + for c in cs: + component_count += 1 + + value = str(c.value) + part = c.find_field("part") + digikey = c.find_field("digikey") + footprint = c.footprint + + if c.ref_class is not None: + ref_class = c.ref_class + ref_instance = c.ref_instance + else: + ref_class = c.ref + ref_instance = "" + + # cli.info(fmt % (ref_class, ref_instance)) + # cli.info(' Value: %s' % c.value.text) + # if c.footprint: + # cli.info(' Footprint: %s' % c.footprint) + + if part is not None: + if part != 'NA': + has_part += 1 if part is not None else 0 + part_count += 1 + + if digikey is not None: + has_digikey += 1 + else: + part = None + else: + part_count += 1 + part = "MISSING" + + # if part is not None: + # cli.info(' Part: %s' % part) + # if digikey is not None: + # cli.info(' Digikey: %s' % digikey) + + schematic_url = args.schematic_url if hasattr(args, 'schematic_url') else export.generate_schematic_url() + + # cli.info() + # cli.info('=== Summary ===') + # cli.info('Schematic URL: %s' % schematic_url) + # cli.info('Number of components: %d' % component_count) + # cli.info('Number of parts: %d' % part_count) + # cli.info('Assigned part: %d / %2.f%%' % (has_part, (has_part / part_count) * 100)) + # cli.info('Assigned digikey: %d / %2.f%%' % (has_digikey, (has_digikey / part_count) * 100)) + + from ..kicad import rdf as kicad_rdf + from rdflib import Literal, BNode, URIRef + from rdflib.namespace import RDF, RDFS + + g = cli.create_graph(kicad=True) + + schematic = URIRef(schematic_url) + g.add((schematic, RDF.type, kicad_rdf.KICAD_TYPE.schematic)) + + # componentNodes = [] + for c in export.components: + node = BNode() + g.add((schematic, kicad_rdf.KICAD.component, node)) + + g.add((node, RDF.type, kicad_rdf.KICAD_TYPE.schematic_component)) + g.add((node, RDFS.label, Literal(c.ref))) + g.add((node, kicad_rdf.KICAD.value, Literal(c.value.text))) + + for name, value in c.fields.items(): + f = BNode() + g.add((node, kicad_rdf.KICAD.field, f)) + g.add((f, RDF.type, kicad_rdf.KICAD_TYPE.field)) + g.add((f, kicad_rdf.KICAD.field_name, Literal(name))) + g.add((f, kicad_rdf.KICAD.field_value, Literal(value))) + + # TODO: serialize the data from too + + g.serialize(destination=dst, encoding='utf-8', format='turtle') -- cgit v1.2.3