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 from typing import List import rdflib from rdflib import Literal, URIRef from rdflib.namespace import RDF, RDFS from .. import cli from ..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 # type: str self.value = Value.parse(value) # type: Value self.footprint = footprint # type: str 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() # type: Design self.components = components # type: List[Component] @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' footprints = set() 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))) footprint_uri = URIRef(kicad_rdf.KICAD_FOOTPRINT[quote_plus(c.footprint)]) if not footprint_uri in footprints: g.add((footprint_uri, RDF.type, kicad_rdf.KICAD_TYPE.footprint)) g.add((footprint_uri, RDFS.label, Literal(c.footprint))) footprints.add(footprint_uri) g.add((node, kicad_rdf.KICAD.footprint, footprint_uri)) 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')