From 9eba62ef1d6b4896de693976116f69a9692332d9 Mon Sep 17 00:00:00 2001 From: Trygve Laugstøl Date: Mon, 20 May 2019 15:27:38 +0200 Subject: ee: o Adding FactType as a smaller wrapper around the fact uri. o Adding ee.part.Facts, used as Part.facts o Renaming 'type' uri to 'ee-component-type'. kicad-make-bom: Removing strategy functionality, replaced with part-apply-function. Moving default strategy contents into ee.kicad.functions. --- src/ee/__init__.py | 18 +++- src/ee/kicad/functions.py | 105 +++++++++++++++++++++ src/ee/kicad/make_bom.py | 164 +++------------------------------ src/ee/kicad/model.py | 24 ++--- src/ee/kicad/sch_fact_types.py | 18 ++++ src/ee/part/__init__.py | 52 ++++++++++- src/ee/part/common_fact_types.py | 7 ++ src/ee/part/fact_keys.py | 1 + src/ee/part/requirement.py | 58 ++++++++++++ src/ee/tools/kicad_make_bom.py | 4 +- src/ee/tools/part_apply_function.py | 67 ++++++++++++++ src/ee/tools/part_find_requirements.py | 39 ++++++++ src/ee/tools/templates/build.ninja.j2 | 21 ++++- src/ee/xml/uris.py | 2 +- 14 files changed, 407 insertions(+), 173 deletions(-) create mode 100644 src/ee/kicad/functions.py create mode 100644 src/ee/kicad/sch_fact_types.py create mode 100644 src/ee/part/common_fact_types.py create mode 100644 src/ee/part/requirement.py create mode 100644 src/ee/tools/part_apply_function.py create mode 100644 src/ee/tools/part_find_requirements.py diff --git a/src/ee/__init__.py b/src/ee/__init__.py index 93c73f7..2ba113e 100644 --- a/src/ee/__init__.py +++ b/src/ee/__init__.py @@ -8,6 +8,11 @@ from ee.formatting import eng_str __all__ = [ "EeException", "EeVal", + "EeValType", + "resistance_type", + "capacitance_type", + "inductance_type", + "power_type", ] @@ -15,6 +20,18 @@ class EeException(Exception): pass +class EeValType(object): + def __init__(self, symbol, alternate_symbols): + self.symbol = symbol + self.alternate_symbols = alternate_symbols + + +resistance_type = EeValType("\u2126", ["ohm"]) +capacitance_type = EeValType("F", []) +inductance_type = EeValType("H", []) +power_type = EeValType("W", []) + + @total_ordering class EeVal(object): units = ['F', @@ -102,4 +119,3 @@ class EeVal(object): def __float__(self): return self._value * math.pow(10, self._exp) - diff --git a/src/ee/kicad/functions.py b/src/ee/kicad/functions.py new file mode 100644 index 0000000..39614b6 --- /dev/null +++ b/src/ee/kicad/functions.py @@ -0,0 +1,105 @@ +import re + +import ee.kicad.model +import ee.kicad.sch_fact_types as kicad_ft +from ee.kicad import sch_fact_types +from ee.part import Part +from ee.part import common_fact_types +from ee.xml import uris + + +def part_type_from_footprint(lib): + if lib == "Capacitor_SMD": + return uris.CAPACITOR + elif lib == "Resistor_SMD": + return uris.RESISTOR + elif lib == "Diode_SMD": + return uris.DIODE + elif lib == "Inductor_SMD": + return uris.INDUCTOR + elif lib == "Crystal": + return uris.CRYSTAL + + +def part_type_from_ref_type(ref_type): + if ref_type == "C": + return uris.CAPACITOR + elif ref_type == "R": + return uris.RESISTOR + elif ref_type == "D": + return uris.DIODE + elif ref_type == "L": + return uris.INDUCTOR + elif ref_type == "X": + return uris.CRYSTAL + elif ref_type == "Q": + return uris.TRANSISTOR + + +def part_type_strategy(part: Part) -> Part: + pt = None + + fp_lib = part.facts.get_value(kicad_ft.footprint_library) + if fp_lib is not None: + pt = part_type_from_footprint(fp_lib) + + ref = part.get_only_schematic_reference() + if ref: + ref_type, ref_num = ee.kicad.model.split_ref(ref.referenceProp) + + if ref_type: + pt = part_type_from_ref_type(ref_type) + + if pt is not None: + part.facts.add(common_fact_types.ee_component_type, pt) + + return part + + +def fix_value_strategy(part: Part) -> Part: + ref = part.get_only_schematic_reference() + + if not ref: + return part + + ref_type, ref_num = ee.kicad.model.split_ref(ref.referenceProp) + + if not ref_num: + return part + + v = part.facts.get_value(sch_fact_types.value) + if not v: + return part + + symbol_name = part.facts.get_value(sch_fact_types.symbol_name) + + if ref_type in ("D", "R", "L", "C") and v == symbol_name: + part.remove_fact(uris.make_fact_key("value")) + + if ref_type == "Q" and v == symbol_name and re.match("^Q_[NP]MOS_[DSG]{3}$", symbol_name): + part.remove_fact(uris.make_fact_key("value")) + + return part + + +def mpn_strategy(part: Part) -> Part: + for field in part.facts.all(sch_fact_types.field): + + k, v = re.split(":", field.value, 1) + if k == "mpn": + part.add_mpn(v) + + return part + + +def default(part: Part) -> Part: + functions = [ + fix_value_strategy, + mpn_strategy, + part_type_strategy, + ] + + for f in functions: + part = f(part) + + return part diff --git a/src/ee/kicad/make_bom.py b/src/ee/kicad/make_bom.py index dcdf96b..13b2157 100644 --- a/src/ee/kicad/make_bom.py +++ b/src/ee/kicad/make_bom.py @@ -1,145 +1,19 @@ -import pydoc -import re from pathlib import Path -from typing import Optional, List, Callable, Mapping from xml.dom import minidom from xml.etree import ElementTree -from ee import EeException -from ee.kicad.model import Component +from ee.kicad import sch_fact_types from ee.kicad.read_schematic import read_schematics from ee.kicad.to_bom import to_bom, to_bom_xml from ee.part import PartDb, save_db, Part -from ee.xml import types, uris +from ee.xml import types __all__ = [ - "StrategyCallable", - "MakeBomStrategy", - "apply_strategies", - "mpn_strategy", - "part_type_strategy", - "dpn_strategy_factory", "make_bom", ] -StrategyCallable = Callable[[Component, Part], Optional[Part]] - -def apply_strategies(c: Component, part: Part, strategies: List[StrategyCallable]): - for strategy in strategies: - part = strategy(c, part) - - if part is None: - return - - if not isinstance(part, Part): - raise EeException("Values returned from strategy must be a Part, got {}".format(type(part))) - - return part - - -def part_type_from_footprint(fp): - lib, part_name = fp.split(":") - - if lib == "Capacitor_SMD": - return uris.CAPACITOR - elif lib == "Resistor_SMD": - return uris.RESISTOR - elif lib == "Diode_SMD": - return uris.DIODE - elif lib == "Inductor_SMD": - return uris.INDUCTOR - elif lib == "Crystal": - return uris.CRYSTAL - - -def part_type_from_ref_type(ref_type): - if ref_type == "C": - return uris.CAPACITOR - elif ref_type == "R": - return uris.RESISTOR - elif ref_type == "D": - return uris.DIODE - elif ref_type == "L": - return uris.INDUCTOR - elif ref_type == "X": - return uris.CRYSTAL - elif ref_type == "Q": - return uris.TRANSISTOR - - -def part_type_strategy(component: Component, part: Part) -> Part: - pt = None - - fp = component.footprint - if fp is not None: - pt = part_type_from_footprint(fp) - - if pt is None and component.has_ref_num: - pt = part_type_from_ref_type(component.ref_type) - - if pt is not None: - part.get_facts().append(types.Fact(uris.make_fact_key("type"), value=pt)) - - return part - - -def fix_value_strategy(component: Component, part: Part) -> Part: - if not component.has_ref_num: - return part - - rt = component.ref_type - v = component.value - - if rt in ("D", "R", "L", "C") and v == component.symbol.name: - part.remove_fact(uris.make_fact_key("value")) - - if rt == "Q" and v == component.symbol.name and re.match("^Q_[NP]MOS_[DSG]{3}$", component.symbol.name): - part.remove_fact(uris.make_fact_key("value")) - - return part - - -def mpn_strategy(component: Component, part: Part) -> Part: - mpn = component.get_field("mpn") - if mpn is not None: - part.add_mpn(mpn.value) - - return part - - -def dpn_strategy_factory(dpn_mappings: Mapping[str, str]) -> StrategyCallable: - def dpn_strategy(component: Component, part: Part) -> Part: - for field_name, distributor in dpn_mappings: - s = component.get_field(field_name) - if s is None: - continue - - part.add_spn(s.value) - - return part - - return dpn_strategy - - -class MakeBomStrategy(): - def __init__(self): - self.dpn_mappings = {} - self.default_strategies = [ - fix_value_strategy, - mpn_strategy, - part_type_strategy, - dpn_strategy_factory(self.dpn_mappings), - ] - - def process_part(self, component: Component, part: Part): - return self.default_process_part(component, part) - - def default_process_part(self, component: Component, part: Part): - return apply_strategies(component, part, self.default_strategies) - - -def work(sch, out: Path, strategy: MakeBomStrategy, new_mode, pretty): +def work(sch, out: Path, new_mode, pretty): def strip(s): s = (s or "").strip() @@ -165,39 +39,31 @@ def work(sch, out: Path, strategy: MakeBomStrategy, new_mode, pretty): value = strip(c.value) if value: - part.facts.add(uris.make_fact_key("value"), value) + part.facts.add(sch_fact_types.value, value) + + if c.symbol.library: + part.facts.add(sch_fact_types.symbol_library, c.symbol.library) + part.facts.add(sch_fact_types.symbol_name, c.symbol.name) footprint = strip(c.footprint) if footprint: - part.facts.add(uris.make_fact_key("footprint"), footprint) + part.facts.add(sch_fact_types.footprint, footprint) i = footprint.find(":") if i >= 0: lib, footprint = footprint.split(":") - part.facts.add(uris.make_fact_key("kicad-schematic-library"), lib) - part.facts.add(uris.make_fact_key("kicad-schematic-footprint"), footprint) + part.facts.add(sch_fact_types.footprint_library, lib) + part.facts.add(sch_fact_types.footprint_name, footprint) - part = strategy.process_part(c, part) - - if part is None: - continue + for f in c.named_fields: + part.facts.add(sch_fact_types.field, "{}:{}".format(f.name, f.value)) parts.add_entry(part, True) save_db(out, parts) -def make_bom(sch_file: Path, out_dir: Path, strategy_name: str, new_mode: bool, pretty: bool): +def make_bom(sch_file: Path, out_dir: Path, new_mode: bool, pretty: bool): sch = read_schematics(str(sch_file)) - make_bom_strategy_factory = pydoc.locate(strategy_name) - - if not callable(make_bom_strategy_factory): - raise EeException("Not a callable: {}, is a {}".format(strategy_name, type(make_bom_strategy_factory))) - - make_bom_strategy = make_bom_strategy_factory() # type: MakeBomStrategy - - if not isinstance(make_bom_strategy, MakeBomStrategy): - raise EeException("Not a MakeBomStrategy: {}, is a {}".format(strategy_name, type(make_bom_strategy))) - - work(sch, out_dir, make_bom_strategy, new_mode, pretty) + work(sch, out_dir, new_mode, pretty) diff --git a/src/ee/kicad/model.py b/src/ee/kicad/model.py index 295a40a..1f6cf96 100644 --- a/src/ee/kicad/model.py +++ b/src/ee/kicad/model.py @@ -1,6 +1,6 @@ import re from functools import total_ordering -from typing import List, Set +from typing import List, Set, Tuple, Optional from ee import EeException @@ -62,7 +62,16 @@ class Symbol(object): self.name = symbol else: self.library = symbol[0:i] - self.name = symbol[i+1:] + self.name = symbol[i + 1:] + + +def split_ref(ref: str) -> Tuple[Optional[str], Optional[int]]: + r = re.compile("([^0-9]+)(.+)") + try: + parts = r.split(ref) + return parts[1], int(parts[2]) + except ValueError: + return None, None @total_ordering @@ -76,14 +85,7 @@ class Component(object): self._ref = ref self._fields = fields # type List[ComponentField] - r = re.compile("([^0-9]+)(.+)") - try: - parts = r.split(self._ref) - self._ref_type = parts[1] - self._ref_num = int(parts[2]) - except ValueError: - self._ref_type = None - self._ref_num = None + self._ref_type, self._ref_num = split_ref(self._ref) def __eq__(self, o: object) -> bool: other = o # type: Component @@ -157,7 +159,7 @@ class Component(object): return list(self._fields) @property - def named_fields(self): + def named_fields(self) -> List[ComponentField]: return [f for f in self._fields if f.name] def get_field(self, name) -> ComponentField: diff --git a/src/ee/kicad/sch_fact_types.py b/src/ee/kicad/sch_fact_types.py new file mode 100644 index 0000000..c9ea918 --- /dev/null +++ b/src/ee/kicad/sch_fact_types.py @@ -0,0 +1,18 @@ +from ee.part import FactType + + +def make_fact_key(key: str): + return "http://purl.org/ee/kicad-sch-fact-type#{}".format(key) + + +value = FactType(make_fact_key("value"), "Value") +component = FactType(make_fact_key("component"), "Value") + +symbol_library = FactType(make_fact_key("symbol-library"), "Value") +symbol_name = FactType(make_fact_key("symbol-name"), "Value") + +footprint = FactType(make_fact_key("footprint"), "Value") +footprint_library = FactType(make_fact_key("footprint-library"), "Value") +footprint_name = FactType(make_fact_key("footprint-name"), "Value") + +field = FactType(make_fact_key("field"), "Value") diff --git a/src/ee/part/__init__.py b/src/ee/part/__init__.py index ff6c7f1..a2face8 100644 --- a/src/ee/part/__init__.py +++ b/src/ee/part/__init__.py @@ -1,11 +1,13 @@ from pathlib import Path from typing import List, Optional, Iterator, Union -from ee import EeException +from ee import EeException, EeValType from ee.money import Money from ee.xml import types __all__ = [ + "FactType", + "EeValueFactType", "Part", "PartDb", "load_db", @@ -13,6 +15,18 @@ __all__ = [ ] +class FactType(object): + def __init__(self, uri: str, label: str): + self.uri = uri + self.label = label + + +class EeValueFactType(FactType): + def __init__(self, uri: str, label, ee_type: EeValType): + super().__init__(uri, label) + self.ee_type = ee_type + + class Reference(object): pass @@ -324,12 +338,42 @@ class Part(object): self.xml.factsProp.fact = [f for f in self.xml.factsProp.fact if f.keyProp != key] +class Fact(object): + def __init__(self, xml: types.Fact): + self.xml = xml + + @property + def key(self) -> str: + return self.xml.keyProp + + @property + def value(self) -> str: + return self.xml.valueProp + + class Facts(object): - def __init__(self, part): + def __init__(self, part: Part): self.part = part - def add(self, key: str, value: str, label=None): - self.part.get_facts().append(types.Fact(key=key, label=label, value=value)) + @staticmethod + def _get_key(key: Union[str, FactType]): + return key if isinstance(key, str) else key.uri + + def add(self, key: Union[str, FactType], value: str, label=None): + k = self._get_key(key) + self.part.get_facts().append(types.Fact(key=k, label=label, value=value)) + + def get(self, key: Union[str, FactType]) -> Fact: + k = self._get_key(key) + return next((Fact(f) for f in self.part.get_facts() if f.keyProp == k), None) + + def all(self, key: Union[str, FactType]) -> List[Fact]: + k = self._get_key(key) + return [Fact(f) for f in self.part.get_facts() if f.keyProp == k] + + def get_value(self, key: Union[str, FactType]) -> Optional[str]: + k = self._get_key(key) + return next((f.valueProp for f in self.part.get_facts() if f.keyProp == k), None) class Entry(object): diff --git a/src/ee/part/common_fact_types.py b/src/ee/part/common_fact_types.py new file mode 100644 index 0000000..12c099e --- /dev/null +++ b/src/ee/part/common_fact_types.py @@ -0,0 +1,7 @@ +import ee +from ee.part import EeValueFactType, fact_keys + +resistance = EeValueFactType(fact_keys.resistance, "Resistance", ee.resistance_type) +capacitance = EeValueFactType(fact_keys.capacitance, "Capacitance", ee.capacitance_type) + +ee_component_type = EeValueFactType(fact_keys.ee_component_type, "EE component type", ee.capacitance_type) diff --git a/src/ee/part/fact_keys.py b/src/ee/part/fact_keys.py index f1f02c5..65dc699 100644 --- a/src/ee/part/fact_keys.py +++ b/src/ee/part/fact_keys.py @@ -1,3 +1,4 @@ +ee_component_type = "http://purl.org/ee/fact-type/ee-component-type" capacitance = "http://purl.org/ee/fact-type/capacitance" max_voltage = "http://purl.org/ee/fact-type/voltage" # https://en.wikipedia.org/wiki/Ceramic_capacitor#Class_2_ceramic_capacitors diff --git a/src/ee/part/requirement.py b/src/ee/part/requirement.py new file mode 100644 index 0000000..a9381df --- /dev/null +++ b/src/ee/part/requirement.py @@ -0,0 +1,58 @@ +from typing import List + +from ee.part import Part, fact_keys, FactType, common_fact_types + + +class Requirement(object): + def __init__(self, part: Part): + self.part = part + + +class EqualRequirement(Requirement): + def __init__(self, part: Part, fact_type: FactType, value: str): + super().__init__(part) + self.fact_type = fact_type + self.value = value + + def __str__(self): + return "{}.{} == {}".format(self.part.printable_reference, self.fact_type, self.value) + + +class MinRequirement(Requirement): + def __init__(self, part: Part, fact_type: FactType, value: str): + super().__init__(part) + self.fact_type = fact_type + self.value = value + + def __str__(self): + return "{}.{} == {}".format(self.part.printable_reference, self.fact_type, self.value) + + +class MaxRequirement(Requirement): + def __init__(self, part: Part, fact_type: FactType, value: str): + super().__init__(part) + self.fact_type = fact_type + self.value = value + + def __str__(self): + return "{}.{} == {}".format(self.part.printable_reference, self.fact_type, self.value) + + +class PartAnalysis(object): + def __init__(self, part: Part, requirements: List[Requirement]): + self.part = part + self.requirements = requirements + + +def analyze_requirements(part: Part) -> PartAnalysis: + rs = [] + + resistance = part.find_fact(common_fact_types.resistance.uri) + if resistance: + rs.append(EqualRequirement(part, common_fact_types.resistance, resistance.valueProp)) + + capacitance = part.find_fact(common_fact_types.capacitance.uri) + if capacitance: + rs.append(EqualRequirement(part, common_fact_types.capacitance, capacitance.valueProp)) + + return PartAnalysis(part, rs) diff --git a/src/ee/tools/kicad_make_bom.py b/src/ee/tools/kicad_make_bom.py index 60509a3..0e36c6d 100644 --- a/src/ee/tools/kicad_make_bom.py +++ b/src/ee/tools/kicad_make_bom.py @@ -22,6 +22,4 @@ args = parser.parse_args() new_mode = True -strategy = args.strategy if args.strategy else "ee.kicad.make_bom.MakeBomStrategy" - -make_bom(Path(args.sch), Path(args.out), strategy, new_mode, pretty) +make_bom(Path(args.sch), Path(args.out), new_mode, pretty) diff --git a/src/ee/tools/part_apply_function.py b/src/ee/tools/part_apply_function.py new file mode 100644 index 0000000..bece364 --- /dev/null +++ b/src/ee/tools/part_apply_function.py @@ -0,0 +1,67 @@ +import argparse +import pydoc +from pathlib import Path +from typing import List + +from ee import tools, EeException +from ee.part import Part, PartDb, load_db, save_db +from ee.project import Project + + +def load_functions(function_names): + functions = [] + for fn in function_names: + f = pydoc.locate(fn) + + if f is None: + raise EeException("Could not load function: {}".format(fn)) + + if not callable(f): + raise EeException("Not a callable: {}, is a {}".format(fn, type(f))) + + functions.append((fn, f)) + return functions + + +def work(in_path: Path, out_path: Path, report_path: Path, function_names: List[str]): + functions = load_functions(function_names) + + in_parts = load_db(in_path) + + parts = PartDb() + + tools.mk_parents(report_path) + with report_path.open("w") as rpt: + for xml in in_parts.iterparts(): + part = Part(xml) + + for name, f in functions: + part = f(part) + + save_db(out_path, in_parts) + + +parser = argparse.ArgumentParser() + +parser.add_argument("--in", + dest="in_path", + required=True, + metavar="PART DB") + +parser.add_argument("--out", + required=True, + metavar="REQUIREMENTS") + +parser.add_argument("--function", + required=True, + nargs="*", + metavar="FUNCTION") + +parser.add_argument("--execution", + default="default") + +args = parser.parse_args() + +project = Project.load() +report = project.report_dir / "apply-function" / (args.execution + ".rst") +work(Path(args.in_path), Path(args.out), report, args.function) diff --git a/src/ee/tools/part_find_requirements.py b/src/ee/tools/part_find_requirements.py new file mode 100644 index 0000000..032db02 --- /dev/null +++ b/src/ee/tools/part_find_requirements.py @@ -0,0 +1,39 @@ +import argparse +from pathlib import Path + +from ee.part import requirement, Part, PartDb, load_db, save_db + + +def work(in_path: Path, out_path: Path): + in_parts = load_db(in_path) + + with out_path.open("w") as f: + print("", file=f) + for xml in in_parts.iterparts(): + part = Part(xml) + + analysis = requirement.analyze_requirements(part) + + print("Part: {}. Found {} requirements".format(analysis.part.printable_reference, + len(analysis.requirements)), file=f) + + for r in analysis.requirements: + print(" {}".format(r), file=f) + + print("", file=f) + + +parser = argparse.ArgumentParser() + +parser.add_argument("--in", + dest="in_path", + required=True, + metavar="PART DB") + +parser.add_argument("--out", + required=True, + metavar="REQUIREMENTS") + +args = parser.parse_args() + +work(Path(args.in_path), Path(args.out)) diff --git a/src/ee/tools/templates/build.ninja.j2 b/src/ee/tools/templates/build.ninja.j2 index 76da4cf..1f9ba35 100644 --- a/src/ee/tools/templates/build.ninja.j2 +++ b/src/ee/tools/templates/build.ninja.j2 @@ -19,12 +19,19 @@ rule kicad-gerber rule kicad-make-bom description = kicad-make-bom $out - command = $ee kicad-make-bom --sch $sch --out $out $strategy + command = $ee kicad-make-bom --sch $sch --out $out rule pn-part-search-list description = pn-part-search-list supplier: $supplier command = $ee pn-part-search-list --in $in --out $out --supplier $supplier +rule part-apply-function + command = $ee part-apply-function --in $in --out $out $functions + +rule part-find-requirements + description = part-find-requirements + command = $ee part-find-requirements --in $in --out $out + rule digikey-search-parts description = digikey-search-parts command = $ee digikey-search-parts --in $in --out $out @@ -69,11 +76,17 @@ build {{ gerber_zip }}: kicad-gerber $pcb {%- endif %} {% if sch is defined -%} -build ee/sch.xml: kicad-make-bom $sch -{%- if project.cfg["kicad-project"]["strategy"] %} - strategy = --strategy {{ project.cfg["kicad-project"]["strategy"] }} +build ee/kicad-sch.xml: kicad-make-bom $sch +build ee/sch.xml: part-apply-function ee/kicad-sch.xml +{%- if project.cfg["kicad-project"]["functions"] %} + functions = --function {{ project.cfg["kicad-project"]["functions"] }} +{%- else %} + functions = --function ee.kicad.functions.default {%- endif %} {%- endif %} + +build ee/requirements.xml: part-find-requirements ee/sch.xml + {% for s in distributors %} # Supplier {{ s }} build ee/{{ s }}/pn-part-search-list.xml: pn-part-search-list ee/sch.xml diff --git a/src/ee/xml/uris.py b/src/ee/xml/uris.py index f168c59..bb961df 100644 --- a/src/ee/xml/uris.py +++ b/src/ee/xml/uris.py @@ -1,6 +1,6 @@ from typing import Optional -# Values for `..#type` facts +# Values for `..#ee-component-type` facts CAPACITOR = "http://purl.org/ee/part-type#capacitor" RESISTOR = "http://purl.org/ee/part-type#resistor" DIODE = "http://purl.org/ee/part-type#diode" -- cgit v1.2.3