diff --git a/src/faebryk/libs/picker/jlcpcb/jlcpcb.py b/src/faebryk/libs/picker/jlcpcb/jlcpcb.py index 9271c811..d0b53a26 100644 --- a/src/faebryk/libs/picker/jlcpcb/jlcpcb.py +++ b/src/faebryk/libs/picker/jlcpcb/jlcpcb.py @@ -364,6 +364,10 @@ def attach( f"{indent(module.pretty_params(), ' '*4)}" ) + @property + def mfr_name(self) -> str: + return asyncio.run(Manufacturers().get_from_id(self.manufacturer_id)) + class ComponentQuery: class Error(Exception): ... @@ -482,6 +486,8 @@ def filter_by_manufacturer_pn(self, partnumber: str) -> Self: def filter_by_manufacturer(self, manufacturer: str) -> Self: assert self.Q + if not manufacturer: + return self manufacturer_ids = asyncio.run(Manufacturers().get_ids(manufacturer)) self.Q &= Q(manufacturer_id__in=manufacturer_ids) return self diff --git a/src/faebryk/libs/picker/jlcpcb/picker_lib.py b/src/faebryk/libs/picker/jlcpcb/picker_lib.py index 03c35426..845dc2d7 100644 --- a/src/faebryk/libs/picker/jlcpcb/picker_lib.py +++ b/src/faebryk/libs/picker/jlcpcb/picker_lib.py @@ -6,6 +6,7 @@ from faebryk.core.module import Module from faebryk.libs.e_series import E_SERIES_VALUES from faebryk.libs.picker.jlcpcb.jlcpcb import ( + Component, ComponentQuery, MappingParameterDB, ) @@ -13,6 +14,7 @@ DescriptiveProperties, PickError, ) +from faebryk.libs.util import KeyErrorAmbiguous, KeyErrorNotFound logger = logging.getLogger(__name__) @@ -44,7 +46,21 @@ def f(x: str) -> F.Constant[T]: return f -def find_lcsc_part(module: Module): +def find_component_by_lcsc_id(lcsc_id: str) -> Component: + parts = ComponentQuery().filter_by_lcsc_pn(lcsc_id).get() + + if len(parts) < 1: + raise KeyErrorNotFound(f"Could not find part with LCSC part number {lcsc_id}") + + if len(parts) > 1: + raise KeyErrorAmbiguous( + parts, f"Found multiple parts with LCSC part number {lcsc_id}" + ) + + return next(iter(parts)) + + +def find_and_attach_by_lcsc_id(module: Module): """ Find a part in the JLCPCB database by its LCSC part number """ @@ -56,23 +72,47 @@ def find_lcsc_part(module: Module): lcsc_pn = module.get_trait(F.has_descriptive_properties).get_properties()["LCSC"] - parts = ComponentQuery().filter_by_lcsc_pn(lcsc_pn).get() - - if len(parts) < 1: - raise PickError(f"Could not find part with LCSC part number {lcsc_pn}", module) - - if len(parts) > 1: + try: + part = find_component_by_lcsc_id(lcsc_pn) + except KeyErrorNotFound as e: + raise PickError( + f"Could not find part with LCSC part number {lcsc_pn}", module + ) from e + except KeyErrorAmbiguous: raise PickError(f"Found no exact match for LCSC part number {lcsc_pn}", module) - if parts[0].stock < qty: + if part.stock < qty: raise PickError( f"Part with LCSC part number {lcsc_pn} has insufficient stock", module ) - parts[0].attach(module, []) + part.attach(module, []) -def find_manufacturer_part(module: Module): +def find_component_by_mfr(mfr: str, mfr_pn: str) -> Component: + parts = ( + ComponentQuery() + .filter_by_manufacturer_pn(mfr_pn) + .filter_by_manufacturer(mfr) + .filter_by_stock(qty) + .sort_by_price() + .get() + ) + + if len(parts) < 1: + raise KeyErrorNotFound( + f"Could not find part with manufacturer part number {mfr_pn}" + ) + + if len(parts) > 1: + raise KeyErrorAmbiguous( + parts, f"Found multiple parts with manufacturer part number {mfr_pn}" + ) + + return next(iter(parts)) + + +def find_and_attach_by_mfr(module: Module): """ Find a part in the JLCPCB database by its manufacturer part number """ @@ -97,19 +137,14 @@ def find_manufacturer_part(module: Module): DescriptiveProperties.manufacturer ] - parts = ( - ComponentQuery() - .filter_by_manufacturer_pn(mfr_pn) - .filter_by_manufacturer(mfr) - .filter_by_stock(qty) - .sort_by_price() - .get() - ) - - if len(parts) < 1: + try: + parts = [find_component_by_mfr(mfr, mfr_pn)] + except KeyErrorNotFound as e: raise PickError( f"Could not find part with manufacturer part number {mfr_pn}", module - ) + ) from e + except KeyErrorAmbiguous as e: + parts = e.duplicates for part in parts: try: diff --git a/src/faebryk/libs/picker/jlcpcb/pickers.py b/src/faebryk/libs/picker/jlcpcb/pickers.py index 333f7d41..376f4d3c 100644 --- a/src/faebryk/libs/picker/jlcpcb/pickers.py +++ b/src/faebryk/libs/picker/jlcpcb/pickers.py @@ -86,8 +86,8 @@ def add_jlcpcb_pickers(module: Module, base_prio: int = 0) -> None: # Generic pickers prio = base_prio - module.add(F.has_multi_picker(prio, JLCPCBPicker(P.find_lcsc_part))) - module.add(F.has_multi_picker(prio, JLCPCBPicker(P.find_manufacturer_part))) + module.add(F.has_multi_picker(prio, JLCPCBPicker(P.find_and_attach_by_lcsc_id))) + module.add(F.has_multi_picker(prio, JLCPCBPicker(P.find_and_attach_by_mfr))) # Type specific pickers prio = base_prio + 1 diff --git a/src/faebryk/libs/pycodegen.py b/src/faebryk/libs/pycodegen.py index 88bc05d4..2b89a729 100644 --- a/src/faebryk/libs/pycodegen.py +++ b/src/faebryk/libs/pycodegen.py @@ -3,11 +3,17 @@ import logging import re +import subprocess +from pathlib import Path +from textwrap import dedent +from typing import Callable, Iterable + +import black logger = logging.getLogger(__name__) -def sanitize_name(raw): +def sanitize_name(raw, expect_arithmetic: bool = False): sanitized = raw # braces sanitized = sanitized.replace("(", "") @@ -18,6 +24,9 @@ def sanitize_name(raw): sanitized = sanitized.replace(".", "_") sanitized = sanitized.replace(",", "_") sanitized = sanitized.replace("/", "_") + if not expect_arithmetic: + sanitized = sanitized.replace("-", "_") + # special symbols sanitized = sanitized.replace("'", "") sanitized = sanitized.replace("*", "") @@ -62,3 +71,45 @@ def handle_unknown_invalid_symbold(match): return None, to_escape return sanitized + + +def gen_repeated_block[T]( + generator: Iterable[T], + func: Callable[[T], str] = dedent, + requires_pass: bool = False, +) -> str: + lines = list(map(func, generator)) + + if not lines and requires_pass: + lines = ["pass"] + + return gen_block("\n".join(lines)) + + +def gen_block(payload: str): + return f"#__MARK_BLOCK_BEGIN\n{payload}\n#__MARK_BLOCK_END" + + +def fix_indent(text: str) -> str: + indent_stack = [""] + + out_lines = [] + for line in text.splitlines(): + if "#__MARK_BLOCK_BEGIN" in line: + indent_stack.append(line.removesuffix("#__MARK_BLOCK_BEGIN")) + elif "#__MARK_BLOCK_END" in line: + indent_stack.pop() + else: + out_lines.append(indent_stack[-1] + line) + + return dedent("\n".join(out_lines)) + + +def format_and_write(code: str, path: Path): + code = code.strip() + code = black.format_file_contents(code, fast=True, mode=black.FileMode()) + path.write_text(code) + + print("Ruff----") + subprocess.run(["ruff", "check", "--fix", path], check=True) + print("--------") diff --git a/src/faebryk/tools/libadd.py b/src/faebryk/tools/libadd.py index 937b5ded..765288aa 100644 --- a/src/faebryk/tools/libadd.py +++ b/src/faebryk/tools/libadd.py @@ -2,14 +2,30 @@ # SPDX-License-Identifier: MIT import glob +import re +import sys from dataclasses import dataclass from pathlib import Path from textwrap import dedent -import black import typer - +from more_itertools import partition + +from faebryk.libs.picker.jlcpcb.jlcpcb import Component +from faebryk.libs.picker.jlcpcb.picker_lib import ( + find_component_by_lcsc_id, + find_component_by_mfr, +) +from faebryk.libs.picker.lcsc import download_easyeda_info +from faebryk.libs.pycodegen import ( + fix_indent, + format_and_write, + gen_block, + gen_repeated_block, + sanitize_name, +) from faebryk.libs.tools.typer import typer_callback +from faebryk.libs.util import KeyErrorAmbiguous, KeyErrorNotFound, find # TODO use AST instead of string @@ -18,6 +34,7 @@ class CTX: path: Path pypath: str + overwrite: bool def get_ctx(ctx: typer.Context) -> "CTX": @@ -28,9 +45,11 @@ def write(ctx: typer.Context, contents: str, filename=""): path: Path = get_ctx(ctx).path if filename: path = path.with_stem(filename) - contents = contents.strip() - contents = black.format_file_contents(contents, fast=True, mode=black.FileMode()) - path.write_text(contents) + + if path.exists() and not get_ctx(ctx).overwrite: + raise FileExistsError(f"File {path} already exists") + + format_and_write(contents, path) typer.echo(f"File {path} created") @@ -72,43 +91,158 @@ def main(ctx: typer.Context, name: str, local: bool = True, overwrite: bool = Fa f"Library folder {libfolder} not found, are you in the right directory?" ) - path = (libfolder / name).with_suffix(".py") + path = libfolder / (name + ".py") - if path.exists() and not overwrite: - raise FileExistsError(f"File {path} already exists") - - ctx.obj = CTX(path=path, pypath=pypath) + ctx.obj = CTX(path=path, pypath=pypath, overwrite=overwrite) @main.command() -def module(ctx: typer.Context, interface: bool = False): +def module( + ctx: typer.Context, interface: bool = False, mfr: bool = False, lcsc: bool = False +): + name = get_name(ctx) + + docstring = "TODO: Docstring describing your module" base = "Module" if not interface else "ModuleInterface" - out = dedent(f""" + part: Component | None = None + traits = [] + nodes = [] + + imports = [ + "import faebryk.library._F as F # noqa: F401", + f"from faebryk.core.{base.lower()} import {base}", + "from faebryk.libs.library import L # noqa: F401", + "from faebryk.libs.units import P # noqa: F401", + ] + + if mfr and lcsc: + raise ValueError("Cannot use both mfr and lcsc") + if mfr or lcsc: + import faebryk.libs.picker.lcsc as lcsc_ + + BUILD_DIR = Path("./build") + lcsc_.BUILD_FOLDER = BUILD_DIR + lcsc_.LIB_FOLDER = BUILD_DIR / Path("kicad/libs") + lcsc_.MODEL_PATH = None + + if lcsc: + part = find_component_by_lcsc_id(name) + traits.append( + "lcsc_id = L.f_field(F.has_descriptive_properties_defined)" + f"({{'LCSC': '{name}'}})" + ) + elif mfr: + if "," in name: + mfr_, mfr_pn = name.split(",", maxsplit=1) + else: + mfr_, mfr_pn = "", name + try: + part = find_component_by_mfr(mfr_, mfr_pn) + except KeyErrorAmbiguous as e: + # try find exact match + try: + part = find(e.duplicates, lambda x: x.mfr == mfr_pn) + except KeyErrorNotFound: + print( + f"Error: Ambiguous mfr_pn({mfr_pn}):" + f" {[x.mfr for x in e.duplicates]}" + ) + print("Tip: Specify the full mfr_pn of your choice") + sys.exit(1) + + if part: + name = sanitize_name(f"{part.mfr_name}_{part.mfr}") + assert isinstance(name, str) + ki_footprint, _, easyeda_footprint, _, easyeda_symbol = download_easyeda_info( + part.partno, get_model=False + ) + + designator_prefix = easyeda_symbol.info.prefix.replace("?", "") + traits.append( + f"designator_prefix = L.f_field(F.has_designator_prefix_defined)" + f"('{designator_prefix}')" + ) + + imports.append("from faebryk.libs.picker.picker import DescriptiveProperties") + traits.append( + f"descriptive_properties = L.f_field(F.has_descriptive_properties_defined)" + f"({{DescriptiveProperties.manufacturer: '{part.mfr_name}', " + f"DescriptiveProperties.partno: '{part.mfr}'}})" + ) + + if part.datasheet: + traits.append( + f"datasheet = L.f_field(F.has_datasheet_defined)('{part.datasheet}')" + ) + + partdoc = part.description.replace(" ", "\n") + docstring = f"{docstring}\n\n{partdoc}" + + # pins -------------------------------- + pins = [ + (pin.settings.spice_pin_number, pin.name.text) + for pin in easyeda_symbol.pins + ] + pins, noname = partition(lambda x: re.match(r"[0-9]+", x[1]), pins) + pins = sorted(pins, key=lambda x: x[0]) + noname = list(noname) + assert all(k == v for k, v in noname) + noname = [k for k, v in sorted(noname, key=lambda x: int(x[0]))] + noname = {pin_num: i for i, pin_num in enumerate(noname)} + + interface_names = { + pin_name: sanitize_name(pin_name) + for _, pin_name in sorted(pins, key=lambda x: x[1]) + } + + nodes.append( + "#TODO: Change auto-generated interface types to actual high level types" + ) + nodes += [f"{pin_name}: F.Electrical" for pin_name in interface_names.values()] + if noname: + nodes.append(f"unnamed = L.list_field({len(noname)}, F.Electrical)") + + traits.append(f""" + @L.rt_field + def pin_association_heuristic(self): + return F.has_pin_association_heuristic_lookup_table( + mapping={{ + {", ".join([f"self.{interface_names[pin_name]}: ['{pin_name}']" + for _,pin_name in pins] + [ + f"self.unnamed[{i}]: ['{pin_num}']" + for pin_num, i in noname.items() + ])} + }}, + accept_prefix=False, + case_sensitive=False, + ) + """) + + out = fix_indent(f""" # This file is part of the faebryk project # SPDX-License-Identifier: MIT import logging - import faebryk.library._F as F # noqa: F401 - from faebryk.core.{base.lower()} import {base} - from faebryk.libs.library import L # noqa: F401 - from faebryk.libs.units import P # noqa: F401 + {gen_repeated_block(imports)} logger = logging.getLogger(__name__) - class {get_name(ctx)}({base}): + class {name}({base}): \"\"\" - Docstring describing your module + {gen_block(docstring)} \"\"\" # ---------------------------------------- # modules, interfaces, parameters # ---------------------------------------- + {gen_repeated_block(nodes)} # ---------------------------------------- # traits # ---------------------------------------- + {gen_repeated_block(traits)} def __preinit__(self): # ------------------------------------ @@ -121,7 +255,7 @@ def __preinit__(self): pass """) - write(ctx, out) + write(ctx, out, filename=name) @main.command()