aboutsummaryrefslogtreecommitdiff
path: root/trygvis/eda/cli/kicad_bom_to_ttl.py
diff options
context:
space:
mode:
authorTrygve Laugstøl <trygvis@inamo.no>2016-12-27 01:14:37 +0100
committerTrygve Laugstøl <trygvis@inamo.no>2016-12-27 01:14:37 +0100
commit67013ae17af0436b930dce450a813239be969601 (patch)
tree93a8b34c93080a27674456481d31d6d8b47266cf /trygvis/eda/cli/kicad_bom_to_ttl.py
downloadeda-rdf-67013ae17af0436b930dce450a813239be969601.tar.gz
eda-rdf-67013ae17af0436b930dce450a813239be969601.tar.bz2
eda-rdf-67013ae17af0436b930dce450a813239be969601.tar.xz
eda-rdf-67013ae17af0436b930dce450a813239be969601.zip
o Initial import of some tools for working with KiCAD BOM files and Digi-key.
Diffstat (limited to 'trygvis/eda/cli/kicad_bom_to_ttl.py')
-rwxr-xr-xtrygvis/eda/cli/kicad_bom_to_ttl.py356
1 files changed, 356 insertions, 0 deletions
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 <design> too
+
+ g.serialize(destination=dst, encoding='utf-8', format='turtle')