diff --git a/src/faebryk/exporters/pcb/kicad/pcb.py b/src/faebryk/exporters/pcb/kicad/pcb.py index 2c218941..84ae2324 100644 --- a/src/faebryk/exporters/pcb/kicad/pcb.py +++ b/src/faebryk/exporters/pcb/kicad/pcb.py @@ -52,6 +52,10 @@ def _get_footprint( return C_kicad_footprint_file.loads(path).footprint +# TODO remove prints +# TODO use G instead of netlist intermediary + + class PCB: @staticmethod def apply_netlist(pcb_path: Path, netlist_path: Path): @@ -83,7 +87,70 @@ def apply_netlist(pcb_path: Path, netlist_path: Path): # - components matched by ref (no fallback) # - if pad no net, just dont put net in sexp + # Nets nl_nets = {n.name: n for n in netlist.export.nets.nets if n.nodes} + 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_net, pads = pcb_nets[old_name] + pcb_net.name = new_name + pcb_nets[new_name] = (pcb_net, pads) + del pcb_nets[old_name] + for pad in pads: + 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), + ) + pcb.kicad_pcb.nets.append(pcb_net) + pcb_nets[net_name] = (pcb_net, []) # Components pcb_comps = { @@ -138,7 +205,7 @@ def apply_netlist(pcb_path: Path, netlist_path: Path): 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, + number=pcb_nets[pads[p.name].name][0].number, name=pads[p.name].name, ) if p.name in pads @@ -164,68 +231,6 @@ def apply_netlist(pcb_path: Path, netlist_path: Path): 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/library/has_descriptive_properties_defined.py b/src/faebryk/library/has_descriptive_properties_defined.py index 3fdea1d0..794c0fc2 100644 --- a/src/faebryk/library/has_descriptive_properties_defined.py +++ b/src/faebryk/library/has_descriptive_properties_defined.py @@ -2,15 +2,17 @@ # SPDX-License-Identifier: MIT +from typing import Mapping + import faebryk.library._F as F from faebryk.core.node import Node from faebryk.core.trait import TraitImpl class has_descriptive_properties_defined(F.has_descriptive_properties.impl()): - def __init__(self, properties: dict[str, str]) -> None: + def __init__(self, properties: Mapping[str, str]) -> None: super().__init__() - self.properties = properties + self.properties = dict(properties) def get_properties(self) -> dict[str, str]: return self.properties diff --git a/src/faebryk/libs/app/pcb.py b/src/faebryk/libs/app/pcb.py index 48afaae5..01cec010 100644 --- a/src/faebryk/libs/app/pcb.py +++ b/src/faebryk/libs/app/pcb.py @@ -19,9 +19,14 @@ C_kicad_pcb_file, C_kicad_project_file, ) +from faebryk.libs.util import ConfigFlag logger = logging.getLogger(__name__) +PCBNEW_AUTO = ConfigFlag( + "PCBNEW_AUTO", default=True, descr="Automatically open pcbnew when applying netlist" +) + def apply_layouts(app: Module): if not app.has_trait(F.has_pcb_position): @@ -89,6 +94,13 @@ def apply_design( pcb.dumps(pcb_path) print("Reopen PCB in kicad") + if PCBNEW_AUTO: + try: + open_pcb(pcb_path) + except FileNotFoundError: + print(f"PCB location: {pcb_path}") + else: + print(f"PCB location: {pcb_path}") def include_footprints(pcb_path: Path): @@ -167,6 +179,8 @@ def open_pcb(pcb_path: os.PathLike): def apply_netlist(pcb_path: Path, netlist_path: Path, netlist_has_changed: bool = True): + from faebryk.exporters.pcb.kicad.pcb import PCB + include_footprints(pcb_path) # Set netlist path in gui menu @@ -184,22 +198,4 @@ def apply_netlist(pcb_path: Path, netlist_path: Path, netlist_has_changed: bool if not netlist_has_changed: return - print("Importing netlist manually...") - - auto_mode = os.environ.get("FBRK_NETLIST_PCBNEW_AUTO", "y").lower() in [ - "y", - "1", - ] - - if auto_mode: - try: - open_pcb(pcb_path) - except FileNotFoundError: - print(f"PCB location: {pcb_path}") - else: - print(f"PCB location: {pcb_path}") - - input( - "Load the netlist in File->Import->Netlist: Update PCB\n" - "Then press ENTER to continue..." - ) + PCB.apply_netlist(pcb_path, netlist_path) diff --git a/src/faebryk/libs/kicad/fileformats.py b/src/faebryk/libs/kicad/fileformats.py index bccbb2d5..c6b9c18f 100644 --- a/src/faebryk/libs/kicad/fileformats.py +++ b/src/faebryk/libs/kicad/fileformats.py @@ -498,6 +498,34 @@ class C_xy: x: float = field(**sexp_field(positional=True)) y: float = field(**sexp_field(positional=True)) + def __sub__(self, other: "C_xy") -> "C_xy": + return C_xy(x=self.x - other.x, y=self.y - other.y) + + def __add__(self, other: "C_xy") -> "C_xy": + return C_xy(x=self.x + other.x, y=self.y + other.y) + + def rotate(self, center: "C_xy", angle: float) -> "C_xy": + import math + + angle = -angle # rotate kicad style counter-clockwise + + # Translate point to origin + translated_x = self.x - center.x + translated_y = self.y - center.y + + # Convert angle to radians + angle = math.radians(angle) + + # Rotate + rotated_x = translated_x * math.cos(angle) - translated_y * math.sin(angle) + rotated_y = translated_x * math.sin(angle) + translated_y * math.cos(angle) + + # Translate back + new_x = rotated_x + center.x + new_y = rotated_y + center.y + + return C_xy(x=new_x, y=new_y) + @dataclass class C_xyz: @@ -702,8 +730,9 @@ class E_shape(SymEnum): **sexp_field(positional=True), default=E_shape.circle ) size_x: Optional[float] = field(**sexp_field(positional=True), default=None) - size_y: Optional[float] = field(**sexp_field(positional=True), default=None) - offset: Optional[C_xy] = None + # TODO reenable + # size_y: Optional[float] = field(**sexp_field(positional=True), default=None) + # offset: Optional[C_xy] = None # TODO: replace with generic gr item @dataclass(kw_only=True) @@ -779,8 +808,8 @@ class C_kicad_pcb_file(SEXP_File): class C_kicad_pcb: @dataclass class C_general: - thickness: float - legacy_teardrops: bool + thickness: float = 1.6 + legacy_teardrops: bool = False @dataclass class C_layer: @@ -797,41 +826,41 @@ class E_type(SymEnum): class C_setup: @dataclass class C_pcbplotparams: - layerselection: str - plot_on_all_layers_selection: str - disableapertmacros: bool - usegerberextensions: bool - usegerberattributes: bool - usegerberadvancedattributes: bool - creategerberjobfile: bool - dashed_line_dash_ratio: float - dashed_line_gap_ratio: float - svgprecision: int - plotframeref: bool - viasonmask: bool - mode: int - useauxorigin: bool - hpglpennumber: int - hpglpenspeed: int - hpglpendiameter: float - pdf_front_fp_property_popups: bool - pdf_back_fp_property_popups: bool - dxfpolygonmode: bool - dxfimperialunits: bool - dxfusepcbnewfont: bool - psnegative: bool - psa4output: bool - plotreference: bool - plotvalue: bool - plotfptext: bool - plotinvisibletext: bool - sketchpadsonfab: bool - subtractmaskfromsilk: bool - outputformat: int - mirror: bool - drillshape: int - scaleselection: int - outputdirectory: str + layerselection: str = "0x00010fc_ffffffff" + plot_on_all_layers_selection: str = "0x0000000_00000000" + disableapertmacros: bool = False + usegerberextensions: bool = False + usegerberattributes: bool = True + usegerberadvancedattributes: bool = True + creategerberjobfile: bool = True + dashed_line_dash_ratio: float = 12.0 + dashed_line_gap_ratio: float = 3.0 + svgprecision: int = 4 + plotframeref: bool = False + viasonmask: bool = False + mode: int = 1 + useauxorigin: bool = False + hpglpennumber: int = 1 + hpglpenspeed: int = 20 + hpglpendiameter: float = 15.0 + pdf_front_fp_property_popups: bool = True + pdf_back_fp_property_popups: bool = True + dxfpolygonmode: bool = True + dxfimperialunits: bool = True + dxfusepcbnewfont: bool = True + psnegative: bool = False + psa4output: bool = False + plotreference: bool = True + plotvalue: bool = True + plotfptext: bool = True + plotinvisibletext: bool = False + sketchpadsonfab: bool = False + subtractmaskfromsilk: bool = False + outputformat: int = 1 + mirror: bool = False + drillshape: int = 1 + scaleselection: int = 1 + outputdirectory: str = "" @dataclass class C_stackup: @@ -874,9 +903,9 @@ class E_copper_finish(StrEnum): edge_plating: Optional[bool] = None stackup: Optional[C_stackup] = None - pad_to_mask_clearance: int - allow_soldermask_bridges_in_footprints: bool - pcbplotparams: C_pcbplotparams + pad_to_mask_clearance: int = 0 + allow_soldermask_bridges_in_footprints: bool = False + pcbplotparams: C_pcbplotparams = field(default_factory=C_pcbplotparams) @dataclass class C_net: @@ -889,8 +918,8 @@ class C_pcb_footprint(C_footprint): class C_pad(C_footprint.C_pad): @dataclass class C_net: - number: int = field(**sexp_field(positional=True), default=0) - name: str = field(**sexp_field(positional=True), default="") + number: int = field(**sexp_field(positional=True)) + name: str = field(**sexp_field(positional=True)) net: Optional[C_net] = None uuid: UUID = field(default_factory=gen_uuid) @@ -1024,12 +1053,175 @@ class C_arc_segment(C_segment): version: int = field(**sexp_field(assert_value=20240108)) generator: str generator_version: str - general: C_general - paper: str - layers: list[C_layer] - setup: C_setup + general: C_general = field(default_factory=C_general) + paper: str = field(default="A4") + layers: list[C_layer] = field( + default_factory=lambda: [ + C_kicad_pcb_file.C_kicad_pcb.C_layer( + number=0, + name="F.Cu", + type=C_kicad_pcb_file.C_kicad_pcb.C_layer.E_type.signal, + ), + C_kicad_pcb_file.C_kicad_pcb.C_layer( + number=31, + name="B.Cu", + type=C_kicad_pcb_file.C_kicad_pcb.C_layer.E_type.signal, + ), + C_kicad_pcb_file.C_kicad_pcb.C_layer( + number=32, + name="B.Adhes", + type=C_kicad_pcb_file.C_kicad_pcb.C_layer.E_type.user, + alias="B.Adhesive", + ), + C_kicad_pcb_file.C_kicad_pcb.C_layer( + number=33, + name="F.Adhes", + type=C_kicad_pcb_file.C_kicad_pcb.C_layer.E_type.user, + alias="F.Adhesive", + ), + C_kicad_pcb_file.C_kicad_pcb.C_layer( + number=34, + name="B.Paste", + type=C_kicad_pcb_file.C_kicad_pcb.C_layer.E_type.user, + ), + C_kicad_pcb_file.C_kicad_pcb.C_layer( + number=35, + name="F.Paste", + type=C_kicad_pcb_file.C_kicad_pcb.C_layer.E_type.user, + ), + C_kicad_pcb_file.C_kicad_pcb.C_layer( + number=36, + name="B.SilkS", + type=C_kicad_pcb_file.C_kicad_pcb.C_layer.E_type.user, + alias="B.Silkscreen", + ), + C_kicad_pcb_file.C_kicad_pcb.C_layer( + number=37, + name="F.SilkS", + type=C_kicad_pcb_file.C_kicad_pcb.C_layer.E_type.user, + alias="F.Silkscreen", + ), + C_kicad_pcb_file.C_kicad_pcb.C_layer( + number=38, + name="B.Mask", + type=C_kicad_pcb_file.C_kicad_pcb.C_layer.E_type.user, + ), + C_kicad_pcb_file.C_kicad_pcb.C_layer( + number=39, + name="F.Mask", + type=C_kicad_pcb_file.C_kicad_pcb.C_layer.E_type.user, + ), + C_kicad_pcb_file.C_kicad_pcb.C_layer( + number=40, + name="Dwgs.User", + type=C_kicad_pcb_file.C_kicad_pcb.C_layer.E_type.user, + alias="User.Drawings", + ), + C_kicad_pcb_file.C_kicad_pcb.C_layer( + number=41, + name="Cmts.User", + type=C_kicad_pcb_file.C_kicad_pcb.C_layer.E_type.user, + alias="User.Comments", + ), + C_kicad_pcb_file.C_kicad_pcb.C_layer( + number=42, + name="Eco1.User", + type=C_kicad_pcb_file.C_kicad_pcb.C_layer.E_type.user, + alias="User.Eco1", + ), + C_kicad_pcb_file.C_kicad_pcb.C_layer( + number=43, + name="Eco2.User", + type=C_kicad_pcb_file.C_kicad_pcb.C_layer.E_type.user, + alias="User.Eco2", + ), + C_kicad_pcb_file.C_kicad_pcb.C_layer( + number=44, + name="Edge.Cuts", + type=C_kicad_pcb_file.C_kicad_pcb.C_layer.E_type.user, + ), + C_kicad_pcb_file.C_kicad_pcb.C_layer( + number=45, + name="Margin", + type=C_kicad_pcb_file.C_kicad_pcb.C_layer.E_type.user, + ), + C_kicad_pcb_file.C_kicad_pcb.C_layer( + number=46, + name="B.CrtYd", + type=C_kicad_pcb_file.C_kicad_pcb.C_layer.E_type.user, + alias="B.Courtyard", + ), + C_kicad_pcb_file.C_kicad_pcb.C_layer( + number=47, + name="F.CrtYd", + type=C_kicad_pcb_file.C_kicad_pcb.C_layer.E_type.user, + alias="F.Courtyard", + ), + C_kicad_pcb_file.C_kicad_pcb.C_layer( + number=48, + name="B.Fab", + type=C_kicad_pcb_file.C_kicad_pcb.C_layer.E_type.user, + ), + C_kicad_pcb_file.C_kicad_pcb.C_layer( + number=49, + name="F.Fab", + type=C_kicad_pcb_file.C_kicad_pcb.C_layer.E_type.user, + ), + C_kicad_pcb_file.C_kicad_pcb.C_layer( + number=50, + name="User.1", + type=C_kicad_pcb_file.C_kicad_pcb.C_layer.E_type.user, + ), + C_kicad_pcb_file.C_kicad_pcb.C_layer( + number=51, + name="User.2", + type=C_kicad_pcb_file.C_kicad_pcb.C_layer.E_type.user, + ), + C_kicad_pcb_file.C_kicad_pcb.C_layer( + number=52, + name="User.3", + type=C_kicad_pcb_file.C_kicad_pcb.C_layer.E_type.user, + ), + C_kicad_pcb_file.C_kicad_pcb.C_layer( + number=53, + name="User.4", + type=C_kicad_pcb_file.C_kicad_pcb.C_layer.E_type.user, + ), + C_kicad_pcb_file.C_kicad_pcb.C_layer( + number=54, + name="User.5", + type=C_kicad_pcb_file.C_kicad_pcb.C_layer.E_type.user, + ), + C_kicad_pcb_file.C_kicad_pcb.C_layer( + number=55, + name="User.6", + type=C_kicad_pcb_file.C_kicad_pcb.C_layer.E_type.user, + ), + C_kicad_pcb_file.C_kicad_pcb.C_layer( + number=56, + name="User.7", + type=C_kicad_pcb_file.C_kicad_pcb.C_layer.E_type.user, + ), + C_kicad_pcb_file.C_kicad_pcb.C_layer( + number=57, + name="User.8", + type=C_kicad_pcb_file.C_kicad_pcb.C_layer.E_type.user, + ), + C_kicad_pcb_file.C_kicad_pcb.C_layer( + number=58, + name="User.9", + type=C_kicad_pcb_file.C_kicad_pcb.C_layer.E_type.user, + ), + ] + ) + setup: C_setup = field(default_factory=C_setup) - nets: list[C_net] = field(**sexp_field(multidict=True), default_factory=list) + nets: list[C_net] = field( + **sexp_field(multidict=True), + default_factory=lambda: [ + C_kicad_pcb_file.C_kicad_pcb.C_net(number=0, name="") + ], + ) footprints: list[C_pcb_footprint] = field( **sexp_field(multidict=True), default_factory=list ) @@ -1141,7 +1333,10 @@ class C_node: ) nets: list[C_net] = field( - **sexp_field(multidict=True), default_factory=list + **sexp_field(multidict=True), + default_factory=lambda: [ + C_kicad_netlist_file.C_netlist.C_nets.C_net(code=0, name="") + ], ) @dataclass diff --git a/src/faebryk/libs/kicad/fileformats_old.py b/src/faebryk/libs/kicad/fileformats_old.py index 8d6a2f32..61dd0741 100644 --- a/src/faebryk/libs/kicad/fileformats_old.py +++ b/src/faebryk/libs/kicad/fileformats_old.py @@ -68,37 +68,21 @@ class C_arc_old: layer: str angle: float - def _calculate_midpoint(self) -> C_xy: - import math + def _calculate_midpoint(self) -> tuple[C_xy, C_xy, C_xy]: + start = self.end + center = self.start - # 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))) + mid = start.rotate(center, -self.angle / 2.0) + end = start.rotate(center, -self.angle) - # 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) + return start, mid, end def convert_to_new(self) -> C_arc: + start, mid, end = self._calculate_midpoint() return C_arc( - start=self.start, - mid=self._calculate_midpoint(), - end=self.end, + start=start, + mid=mid, + end=end, uuid=gen_uuid(), stroke=C_stroke( width=self.width,