From 30bb41fd5f61201cddedae3d3e1ec335a0de8215 Mon Sep 17 00:00:00 2001 From: iopapamanoglou Date: Mon, 9 Sep 2024 19:44:55 +0200 Subject: [PATCH] WIP: Exporter: PCB netlist exporter --- src/faebryk/exporters/pcb/kicad/pcb.py | 231 ++++++++++++++++++ .../exporters/pcb/kicad/transformer.py | 15 +- src/faebryk/libs/app/kicad_netlist.py | 2 +- src/faebryk/libs/app/pcb.py | 2 +- src/faebryk/libs/kicad/fileformats.py | 49 ++-- src/faebryk/libs/kicad/fileformats_old.py | 183 ++++++++++++++ src/faebryk/libs/sexp/dataclass_sexp.py | 23 +- 7 files changed, 471 insertions(+), 34 deletions(-) create mode 100644 src/faebryk/exporters/pcb/kicad/pcb.py create mode 100644 src/faebryk/libs/kicad/fileformats_old.py diff --git a/src/faebryk/exporters/pcb/kicad/pcb.py b/src/faebryk/exporters/pcb/kicad/pcb.py new file mode 100644 index 00000000..02d3c91d --- /dev/null +++ b/src/faebryk/exporters/pcb/kicad/pcb.py @@ -0,0 +1,231 @@ +# This file is part of the faebryk project +# SPDX-License-Identifier: MIT + +import logging +from dataclasses import asdict +from pathlib import Path + +from faebryk.libs.kicad.fileformats import ( + C_footprint, + C_kicad_footprint_file, + C_kicad_fp_lib_table_file, + C_kicad_netlist_file, + C_kicad_pcb_file, + C_xyr, +) +from faebryk.libs.kicad.fileformats_old import C_kicad_footprint_file_easyeda +from faebryk.libs.sexp.dataclass_sexp import get_parent +from faebryk.libs.util import NotNone, find, find_or + +logger = logging.getLogger(__name__) + + +def _nets_same( + pcb_net: tuple[ + C_kicad_pcb_file.C_kicad_pcb.C_net, + list[C_kicad_pcb_file.C_kicad_pcb.C_pcb_footprint.C_pad], + ], + nl_net: C_kicad_netlist_file.C_netlist.C_nets.C_net, +) -> bool: + pcb_pads = { + f"{get_parent(p, C_kicad_pcb_file.C_kicad_pcb.C_pcb_footprint) + .propertys['Reference'].value}.{p.name}" + for p in pcb_net[1] + } + nl_pads = {f"{n.ref}.{n.pin}" for n in nl_net.nodes} + return pcb_pads == nl_pads + + +def _get_footprint( + identifier: str, fp_lib_path: Path +) -> C_kicad_footprint_file.C_footprint_in_file: + fp_lib_table = C_kicad_fp_lib_table_file.loads(fp_lib_path) + lib_id, fp_name = identifier.split(":") + lib = find(fp_lib_table.fp_lib_table.libs, lambda x: x.name == lib_id) + dir_path = Path(lib.uri.replace("${KIPRJMOD}", str(fp_lib_path.parent))) + path = dir_path / f"{fp_name}.kicad_mod" + + # TODO this should be handled in fileformats itself + if path.read_text().startswith("(module"): + return C_kicad_footprint_file_easyeda.loads(path).convert_to_new().footprint + + return C_kicad_footprint_file.loads(path).footprint + + +class PCB: + @staticmethod + def apply_netlist(pcb_path: Path, netlist_path: Path): + from faebryk.exporters.pcb.kicad.transformer import gen_uuid + + fp_lib_path = pcb_path.parent / "fp-lib-table" + + pcb = C_kicad_pcb_file.loads(pcb_path) + netlist = C_kicad_netlist_file.loads(netlist_path) + + # update nets + # load footprints + # - set layer & pos + # - per pad set net + # - load ref & value from netlist + # - set uuid for all (pads, geos, ...) + # - drop fp_text value & fp_text user + + # notes: + # - netcode in netlist unrelated to netcode in pcb + # - matched by name + check for same pads + # - only works if net nodes not changed + # - what happens if net is renamed? + # - segments & vias etc use netcodes + # - if net code removed, recalculated + # -> DIFFICULT: need to know which net/pads they are touching + # - zone & pads uses netcode & netname + # - empty nets ignored (consider removing them from netlist export) + # - components matched by ref (no fallback) + # - if pad no net, just dont put net in sexp + + nl_nets = {n.name: n for n in netlist.export.nets.nets if n.nodes} + + # Components + pcb_comps = { + c.propertys["Reference"].value: c for c in pcb.kicad_pcb.footprints + } + nl_comps = {c.ref: c for c in netlist.export.components.comps} + comps_added = nl_comps.keys() - pcb_comps.keys() + comps_removed = pcb_comps.keys() - nl_comps.keys() + + print("Comps removed:", comps_removed) + for comp_name in comps_removed: + comp = pcb_comps[comp_name] + pcb.kicad_pcb.footprints.remove(comp) + + print("Comps added:", comps_added) + for comp_name in comps_added: + comp = nl_comps[comp_name] + footprint_identifier = comp.footprint + footprint = _get_footprint(footprint_identifier, fp_lib_path) + pads = { + p.pin: n + for n in nl_nets.values() + for p in n.nodes + if p.ref == comp_name + } + + texts = [t for t in footprint.fp_texts if t.type not in ("user", "value")] + for t in texts: + if t.type == "reference": + t.text = comp_name + + propertys: dict[str, C_footprint.C_property] = { + name: C_footprint.C_property( + name=name, + value=k.text, + at=k.at, + layer=k.layer, + uuid=k.uuid, + effects=k.effects, + ) + for k in footprint.fp_texts + if (name := k.type.capitalize()) in ("Reference", "Value") + } + + propertys["Reference"].value = comp_name + propertys["Value"].value = comp.value + + pcb_comp = C_kicad_pcb_file.C_kicad_pcb.C_pcb_footprint( + uuid=gen_uuid(mark=""), + at=C_xyr(x=0, y=0, r=0), + pads=[ + C_kicad_pcb_file.C_kicad_pcb.C_pcb_footprint.C_pad( + uuid=gen_uuid(mark=""), + net=C_kicad_pcb_file.C_kicad_pcb.C_pcb_footprint.C_pad.C_net( + number=pads[p.name].code, + name=pads[p.name].name, + ) + if p.name in pads + else None, + # + **asdict(p), + ) + for p in footprint.pads + ], + # + name=footprint_identifier, + layer=footprint.layer, + propertys=propertys, + attr=footprint.attr, + fp_lines=footprint.fp_lines, + fp_arcs=footprint.fp_arcs, + fp_circles=footprint.fp_circles, + fp_rects=footprint.fp_rects, + fp_texts=texts, + fp_poly=footprint.fp_poly, + model=footprint.model, + ) + + pcb.kicad_pcb.footprints.append(pcb_comp) + + # Nets + pcb_nets = { + n.name: ( + n, + [ + p + for f in pcb.kicad_pcb.footprints + for p in f.pads + if p.net and p.net.name == n.name + ], + ) + for n in pcb.kicad_pcb.nets + if n.name + } + + nets_added = nl_nets.keys() - pcb_nets.keys() + nets_removed = pcb_nets.keys() - nl_nets.keys() + + # Match renamed nets by pads + matched_nets = { + nl_net_name: pcb_net_name + for nl_net_name in nets_added + if ( + pcb_net_name := find_or( + nets_removed, + lambda x: _nets_same(pcb_nets[NotNone(x)], nl_nets[nl_net_name]), + default=None, + ) + ) + } + nets_added.difference_update(matched_nets.keys()) + nets_removed.difference_update(matched_nets.values()) + + if nets_removed: + # TODO + raise NotImplementedError( + f"Nets removed from netlist not implemented: {nets_removed}" + ) + + # Rename nets + print("Renamed nets:", matched_nets) + for new_name, old_name in matched_nets.items(): + pcb_nets[old_name][0].name = new_name + for pad in pcb_nets[old_name][1]: + assert pad.net + pad.net.name = new_name + for zone in pcb.kicad_pcb.zones: + if zone.net_name == old_name: + zone.net_name = new_name + + # Add new nets + print("New nets", nets_added) + for net_name in nets_added: + nl_net = nl_nets[net_name] + pcb_net = C_kicad_pcb_file.C_kicad_pcb.C_net( + name=net_name, + number=len(pcb.kicad_pcb.nets), + ) + # TODO + + pcb.kicad_pcb.nets.append(pcb_net) + + # --- + print("Save PCB", pcb_path) + pcb.dumps(pcb_path) diff --git a/src/faebryk/exporters/pcb/kicad/transformer.py b/src/faebryk/exporters/pcb/kicad/transformer.py index 1647eeef..302ff885 100644 --- a/src/faebryk/exporters/pcb/kicad/transformer.py +++ b/src/faebryk/exporters/pcb/kicad/transformer.py @@ -4,7 +4,6 @@ import logging import pprint import re -import uuid from abc import abstractmethod from dataclasses import fields from enum import Enum, auto @@ -40,6 +39,7 @@ C_xyr, C_xyz, E_fill, + gen_uuid as _gen_uuid, ) from faebryk.libs.sexp.dataclass_sexp import dataclass_dfs from faebryk.libs.util import KeyErrorNotFound, cast_assert, find, get_key @@ -72,18 +72,7 @@ def gen_uuid(mark: str = "") -> UUID: - # format: d864cebe-263c-4d3f-bbd6-bb51c6d2a608 - value = uuid.uuid4().hex - - suffix = mark.encode().hex() - value = value[: -len(suffix)] + suffix - - DASH_IDX = [8, 12, 16, 20] - formatted = value - for i, idx in enumerate(DASH_IDX): - formatted = formatted[: idx + i] + "-" + formatted[idx + i :] - - return UUID(formatted) + return _gen_uuid(mark) def is_marked(uuid: UUID, mark: str): diff --git a/src/faebryk/libs/app/kicad_netlist.py b/src/faebryk/libs/app/kicad_netlist.py index 48ed9798..8d44c470 100644 --- a/src/faebryk/libs/app/kicad_netlist.py +++ b/src/faebryk/libs/app/kicad_netlist.py @@ -4,7 +4,7 @@ import logging from pathlib import Path -from faebryk.core.graph import Graph +from faebryk.core.graphinterface import Graph from faebryk.exporters.netlist.graph import attach_nets_and_kicad_info from faebryk.exporters.netlist.kicad.netlist_kicad import from_faebryk_t2_netlist from faebryk.exporters.netlist.netlist import make_t2_netlist_from_graph diff --git a/src/faebryk/libs/app/pcb.py b/src/faebryk/libs/app/pcb.py index 3899816f..48afaae5 100644 --- a/src/faebryk/libs/app/pcb.py +++ b/src/faebryk/libs/app/pcb.py @@ -8,7 +8,7 @@ from typing import Any, Callable import faebryk.library._F as F -from faebryk.core.graph import Graph +from faebryk.core.graphinterface import Graph from faebryk.core.module import Module from faebryk.core.node import Node from faebryk.exporters.pcb.kicad.transformer import PCB_Transformer diff --git a/src/faebryk/libs/kicad/fileformats.py b/src/faebryk/libs/kicad/fileformats.py index e19ceeb3..bccbb2d5 100644 --- a/src/faebryk/libs/kicad/fileformats.py +++ b/src/faebryk/libs/kicad/fileformats.py @@ -1,4 +1,5 @@ import logging +import uuid from dataclasses import dataclass, field from enum import IntEnum, StrEnum, auto from pathlib import Path @@ -476,6 +477,22 @@ class UUID(str): pass +def gen_uuid(mark: str = ""): + # format: d864cebe-263c-4d3f-bbd6-bb51c6d2a608 + value = uuid.uuid4().hex + + suffix = mark.encode().hex() + if suffix: + value = value[: -len(suffix)] + suffix + + DASH_IDX = [8, 12, 16, 20] + formatted = value + for i, idx in enumerate(DASH_IDX): + formatted = formatted[: idx + i] + "-" + formatted[idx + i :] + + return UUID(formatted) + + @dataclass class C_xy: x: float = field(**sexp_field(positional=True)) @@ -548,65 +565,67 @@ class E_fill(SymEnum): solid = auto() -@dataclass +@dataclass(kw_only=True) class C_line: start: C_xy end: C_xy stroke: C_stroke layer: str - uuid: UUID + uuid: UUID = field(default_factory=gen_uuid) -@dataclass +@dataclass(kw_only=True) class C_circle: center: C_xy end: C_xy stroke: C_stroke fill: E_fill layer: str - uuid: UUID + uuid: UUID = field(default_factory=gen_uuid) -@dataclass +@dataclass(kw_only=True) class C_arc: start: C_xy mid: C_xy end: C_xy stroke: C_stroke layer: str - uuid: UUID + uuid: UUID = field(default_factory=gen_uuid) -@dataclass +@dataclass(kw_only=True) class C_text: text: str = field(**sexp_field(positional=True)) at: C_xyr layer: C_text_layer - uuid: UUID + uuid: UUID = field(default_factory=gen_uuid) effects: C_effects -@dataclass +@dataclass(kw_only=True) class C_fp_text: class E_type(SymEnum): user = auto() + reference = auto() + value = auto() type: E_type = field(**sexp_field(positional=True)) text: str = field(**sexp_field(positional=True)) at: C_xyr layer: C_text_layer - uuid: UUID + uuid: UUID = field(default_factory=gen_uuid) effects: C_effects -@dataclass +@dataclass(kw_only=True) class C_rect: start: C_xy end: C_xy stroke: C_stroke fill: E_fill layer: str - uuid: UUID + uuid: UUID = field(default_factory=gen_uuid) @dataclass @@ -735,7 +754,7 @@ class C_rotate: name: str = field(**sexp_field(positional=True)) layer: str = field(**sexp_field(order=-20)) propertys: dict[str, C_property] = field( - **sexp_field(multidict=True, key=lambda x: x.name) + **sexp_field(multidict=True, key=lambda x: x.name), default_factory=dict ) attr: list[E_attr] fp_lines: list[C_line] = field(**sexp_field(multidict=True), default_factory=list) @@ -874,7 +893,7 @@ class C_net: name: str = field(**sexp_field(positional=True), default="") net: Optional[C_net] = None - uuid: UUID + uuid: UUID = field(default_factory=gen_uuid) uuid: UUID = field(**sexp_field(order=-15)) at: C_xyr = field(**sexp_field(order=-10)) @@ -1050,7 +1069,7 @@ class C_footprint_in_file(C_footprint): generator: str generator_version: str - footprint: C_footprint + footprint: C_footprint_in_file @dataclass diff --git a/src/faebryk/libs/kicad/fileformats_old.py b/src/faebryk/libs/kicad/fileformats_old.py new file mode 100644 index 00000000..df7cca93 --- /dev/null +++ b/src/faebryk/libs/kicad/fileformats_old.py @@ -0,0 +1,183 @@ +import logging +import uuid +from dataclasses import dataclass, field +from typing import cast + +from faebryk.libs.kicad.fileformats import ( + UUID, + C_arc, + C_circle, + C_effects, + C_footprint, + C_fp_text, + C_kicad_footprint_file, + C_line, + C_rect, + C_stroke, + C_xy, + E_fill, + gen_uuid, +) +from faebryk.libs.sexp.dataclass_sexp import SEXP_File, sexp_field + +logger = logging.getLogger(__name__) + + +@dataclass +class C_line_old: + start: C_xy + end: C_xy + layer: str + width: float + + def convert_to_new(self) -> C_line: + return C_line( + start=self.start, + end=self.end, + uuid=gen_uuid(), + layer=self.layer, + stroke=C_stroke( + width=self.width, + type=C_stroke.E_type.solid, + ), + ) + + +@dataclass(kw_only=True) +class C_circle_old: + center: C_xy + end: C_xy + width: float + fill: E_fill = field(default=E_fill.none) + layer: str + + def convert_to_new(self) -> C_circle: + return C_circle( + center=self.center, + end=self.end, + uuid=gen_uuid(), + stroke=C_stroke( + width=self.width, + type=C_stroke.E_type.solid, + ), + fill=self.fill, + layer=self.layer, + ) + + +@dataclass +class C_arc_old: + start: C_xy + end: C_xy + width: float + layer: str + angle: float + + def _calculate_midpoint(self) -> C_xy: + import math + + # Calculate center of the arc + dx = self.end.x - self.start.x + dy = self.end.y - self.start.y + chord_length = math.sqrt(dx**2 + dy**2) + radius = chord_length / (2 * math.sin(math.radians(self.angle / 2))) + + # Midpoint of the chord + mx = (self.start.x + self.end.x) / 2 + my = (self.start.y + self.end.y) / 2 + + # Vector perpendicular to the chord + perpx = -dy / chord_length + perpy = dx / chord_length + + # Calculate the distance from chord midpoint to arc midpoint + sagitta = radius * (1 - math.cos(math.radians(self.angle / 2))) + + # Calculate the midpoint of the arc + midx = mx + sagitta * perpx + midy = my + sagitta * perpy + + return C_xy(x=midx, y=midy) + + def convert_to_new(self) -> C_arc: + return C_arc( + start=self.start, + mid=self._calculate_midpoint(), + end=self.end, + uuid=gen_uuid(), + stroke=C_stroke( + width=self.width, + type=C_stroke.E_type.solid, + ), + layer=self.layer, + ) + + +@dataclass +class C_rect_old: + start: C_xy + end: C_xy + width: float + fill: E_fill + layer: str + + def convert_to_new(self) -> C_rect: + return C_rect( + start=self.start, + end=self.end, + uuid=gen_uuid(), + stroke=C_stroke( + width=self.width, + type=C_stroke.E_type.solid, + ), + fill=self.fill, + layer=self.layer, + ) + + +@dataclass +class C_kicad_footprint_file_easyeda(SEXP_File): + @dataclass(kw_only=True) + class C_footprint_in_file(C_footprint): + tedit: str + + fp_lines: list[C_line_old] = field( + **sexp_field(multidict=True), default_factory=list + ) + fp_arcs: list[C_arc_old] = field( + **sexp_field(multidict=True), default_factory=list + ) + fp_circles: list[C_circle_old] = field( + **sexp_field(multidict=True), default_factory=list + ) + fp_rects: list[C_rect_old] = field( + **sexp_field(multidict=True), default_factory=list + ) + + def convert_to_new(self) -> C_kicad_footprint_file.C_footprint_in_file: + return C_kicad_footprint_file.C_footprint_in_file( + fp_lines=[line.convert_to_new() for line in self.fp_lines], + fp_arcs=[arc.convert_to_new() for arc in self.fp_arcs], + fp_circles=[circle.convert_to_new() for circle in self.fp_circles], + fp_rects=[rect.convert_to_new() for rect in self.fp_rects], + # fp-file + descr="", + tags=[], + version=0, + generator="", + generator_version="", + # fp + name=self.name, + layer=self.layer, + propertys=self.propertys, + attr=self.attr, + fp_texts=self.fp_texts, + fp_poly=self.fp_poly, + pads=self.pads, + model=self.model, + ) + + module: C_footprint_in_file + + def convert_to_new(self) -> C_kicad_footprint_file: + return C_kicad_footprint_file(footprint=self.module.convert_to_new()) diff --git a/src/faebryk/libs/sexp/dataclass_sexp.py b/src/faebryk/libs/sexp/dataclass_sexp.py index 343d3ea5..2a9b60dc 100644 --- a/src/faebryk/libs/sexp/dataclass_sexp.py +++ b/src/faebryk/libs/sexp/dataclass_sexp.py @@ -9,7 +9,7 @@ from sexpdata import Symbol from faebryk.libs.sexp.util import prettify_sexp_string -from faebryk.libs.util import duplicates, groupby, zip_non_locked +from faebryk.libs.util import cast_assert, duplicates, groupby, zip_non_locked logger = logging.getLogger(__name__) @@ -97,7 +97,7 @@ def _convert(val, t): netlist_type = list[netlist_obj] -def _decode[T](sexp: netlist_type, t: type[T]) -> T: +def _decode[T](sexp: netlist_type, t: type[T], parent: Any | None = None) -> T: if logger.isEnabledFor(logging.DEBUG): logger.debug(f"parse into: {t.__name__} {'-'*40}") logger.debug(f"sexp: {sexp}") @@ -154,7 +154,7 @@ def _decode[T](sexp: netlist_type, t: type[T]) -> T: name = f.name sp = sexp_field.from_field(f) if s_name not in key_values: - if sp.multidict and not f.default_factory or f.default: + if sp.multidict and not (f.default_factory or f.default): base_type = get_origin(f.type) or f.type value_dict[name] = base_type() # will be automatically filled by factory @@ -218,7 +218,17 @@ def _decode[T](sexp: netlist_type, t: type[T]) -> T: logger.debug(f"value_dict: {value_dict}") try: - return t(**value_dict) + out = t(**value_dict) + # set parent pointers for all dataclasses in the tree + for k, v in value_dict.items(): + if isinstance(v, list): + vs = v + else: + vs = [v] + for v_ in vs: + if is_dataclass(v_): + setattr(v_, "_parent", out) + return out except TypeError as e: raise TypeError(f"Failed to create {t} with {value_dict}") from e @@ -336,6 +346,11 @@ def dumps(self, path: Path | None = None): return dumps(self, path) +def get_parent[T](obj, t: type[T]) -> T: + assert hasattr(obj, "_parent") + return cast_assert(t, obj._parent) + + # TODO move class JSON_File: @classmethod