#!/usr/bin/env python3 import functools import os.path import re import sys import xml.etree.ElementTree from urllib.parse import quote_plus from operator import attrgetter import itertools import rdflib from .. import cli from ..kicad import rdf as kicad_rdf from rdflib import Literal, URIRef from rdflib.namespace import RDF, RDFS 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(args): if args.input is not None: src = open(args.input, "r") else: src = sys.stdin if args.output is not None: parent = os.path.dirname(args.output) if not os.path.exists(parent): os.mkdir(parent) dst = open(args.output, "wb") else: dst = sys.stdout.buffer schematic_url = args.schematic_url if hasattr(args, 'schematic_url') else None with src, dst: process(src, dst, schematic_url) def process(src, dst, schematic_url): 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) # 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 = schematic_url if schematic_url is not None 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)) g = cli.create_graph(kicad=True) schematic = URIRef(schematic_url) g.add((schematic, RDF.type, kicad_rdf.KICAD_TYPE.schematic)) # The components and fields could/should have been a BNodes. Their generated names are not very nice. # TODO: try using a hash of the current value and put that under a special generated namespace. # "http://example.org/my-board" + ref="C10" => "http://example.org/my-boardC10" # hash("http://example.org/my-boardC10") => 123456 # add_prefix(hash) => "https://trygvis.io/purl/kicad/generated#123456" for c in export.components: ns = rdflib.Namespace(schematic_url) node = ns[c.ref] 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 = ns["%s-%s" % (c.ref, name)] 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')