diff --git a/src/faebryk/library/Header.py b/src/faebryk/library/Header.py index 12e4f4ce..ee384c98 100644 --- a/src/faebryk/library/Header.py +++ b/src/faebryk/library/Header.py @@ -21,8 +21,7 @@ class PadType(Enum): class Angle(Enum): STRAIGHT = auto() - VERTICAL90 = auto() - HORIZONTAL90 = auto() + ANGLE_90 = auto() def __init__( self, @@ -30,14 +29,17 @@ def __init__( vertical_pin_count: int, ) -> None: super().__init__() - self.horizontal_pin_count = horizonal_pin_count - self.vertical_pin_count = vertical_pin_count + self._horizontal_pin_count = horizonal_pin_count + self._vertical_pin_count = vertical_pin_count def __preinit__(self): - self.pin_count_horizonal.merge(self.horizontal_pin_count) - self.pin_count_vertical.merge(self.vertical_pin_count) + self.pin_count_horizonal.merge(self._horizontal_pin_count) + self.pin_count_vertical.merge(self._vertical_pin_count) pin_pitch: F.TBD[Quantity] + mating_pin_lenght: F.TBD[Quantity] + conection_pin_lenght: F.TBD[Quantity] + spacer_height: F.TBD[Quantity] pin_type: F.TBD[PinType] pad_type: F.TBD[PadType] angle: F.TBD[Angle] @@ -46,7 +48,17 @@ def __preinit__(self): pin_count_vertical: F.TBD[int] @L.rt_field - def unnamed(self): - return times(self.horizonal_pin_count * self.vertical_pin_count, F.Electrical) + def contact(self): + return times( + self._horizontal_pin_count * self._vertical_pin_count, F.Electrical + ) - designator_prefix = L.f_field(F.has_designator_prefix_defined)("J") + designator_prefix = L.f_field(F.has_designator_prefix_defined)( + F.has_designator_prefix.Prefix.J + ) + + @L.rt_field + def can_attach_to_footprint(self): + return F.can_attach_to_footprint_via_pinmap( + pinmap={f"{i+1}": self.contact[i] for i in range(len(self.contact))} + ) diff --git a/src/faebryk/library/_F.py b/src/faebryk/library/_F.py index 6c5bb6ec..8bed98ad 100644 --- a/src/faebryk/library/_F.py +++ b/src/faebryk/library/_F.py @@ -76,7 +76,6 @@ from faebryk.library.XtalIF import XtalIF from faebryk.library.has_pin_association_heuristic import has_pin_association_heuristic from faebryk.library.Common_Mode_Filter import Common_Mode_Filter -from faebryk.library.Header import Header from faebryk.library.PJ398SM import PJ398SM from faebryk.library.RJ45_Receptacle import RJ45_Receptacle from faebryk.library.Relay import Relay @@ -114,6 +113,7 @@ from faebryk.library.Capacitor import Capacitor from faebryk.library.Crystal import Crystal from faebryk.library.Fuse import Fuse +from faebryk.library.Header import Header from faebryk.library.Inductor import Inductor from faebryk.library.Resistor import Resistor from faebryk.library.Switch import Switch diff --git a/src/faebryk/libs/picker/jlcpcb/jlcpcb.py b/src/faebryk/libs/picker/jlcpcb/jlcpcb.py index 721754a8..591d93bb 100644 --- a/src/faebryk/libs/picker/jlcpcb/jlcpcb.py +++ b/src/faebryk/libs/picker/jlcpcb/jlcpcb.py @@ -491,6 +491,19 @@ def filter_by_manufacturer_pn(self, partnumber: str) -> Self: self.Q &= Q(mfr__icontains=partnumber) return self + def filter_by_package(self, package: str | list[str]) -> Self: + assert self.Q + + logger.debug(f"Possible package keywords: {package}") + description_query = Q() + if isinstance(package, str): + package = [package] + for keyword in package: + description_query |= Q(package__icontains=keyword) + self.Q &= description_query + + return self + def filter_by_manufacturer(self, manufacturer: str) -> Self: assert self.Q if not manufacturer: diff --git a/src/faebryk/libs/picker/jlcpcb/picker_lib.py b/src/faebryk/libs/picker/jlcpcb/picker_lib.py index 49a421b4..77d5917c 100644 --- a/src/faebryk/libs/picker/jlcpcb/picker_lib.py +++ b/src/faebryk/libs/picker/jlcpcb/picker_lib.py @@ -497,6 +497,112 @@ def find_ldo(cmp: Module): ) +def find_header(cmp: Module): + """ + Find a header part in the JLCPCB database that matches the parameters of the + provided header + """ + + assert isinstance(cmp, F.Header) + + def pin_structure_to_pin_count(vertical: bool) -> Callable[[str], F.Constant[int]]: + """ + Database data looks like: 1x3P + """ + + def f(x: str) -> F.Constant[int]: + if "x" not in x: + raise ValueError(f"Invalid pin structure: {x}") + if vertical: + return F.Constant(int(x.strip("P").split("x")[0])) + else: + return F.Constant(int(x.strip("P").split("x")[1])) + + return f + + def angle_to_mount_type() -> Callable[[str], F.Constant[F.Header.Angle]]: + """ + Database data looks like: "Mounting Type": "Shrouded" or "Straight" + """ + + def f(x: str) -> F.Constant[F.Header.Angle]: + if x == "Shrouded": + return F.Constant(F.Header.Angle.ANGLE_90) + elif x == "Straight": + return F.Constant(F.Header.Angle.STRAIGHT) + else: + raise ValueError(f"Invalid angle: {x}") + + return f + + mapping = [] + if cmp.pad_type.get_most_narrow() == F.Header.PadType.SMD: + pad_type = "SMD" + logger.warning(f"Angle parameter is not supported for SMD header: {cmp}") + else: + pad_type = ["Plugin", "Push-Pull", "TH"] + mapping += [ + MappingParameterDB( + "angle", + ["Mounting Type"], + transform_fn=angle_to_mount_type(), + ), + ] + + if cmp.pin_type.get_most_narrow() == F.Header.PinType.FEMALE: + pin_type = "Female Headers" + pin_count_source = ["Holes Structure"] + elif cmp.pin_type.get_most_narrow() == F.Header.PinType.MALE: + pin_type = "Pin Headers" + pin_count_source = ["Pin Structure"] + mapping = [ + MappingParameterDB( + "mating_pin_lenght", + ["Length of Mating Pin"], + ), + MappingParameterDB( + "conection_pin_lenght", + ["Length of End Connection Pin"], + ), + ] + else: + pin_type = "Headers" + pin_count_source = ["Pin Structure", "Holes Structure"] + + mapping += [ + MappingParameterDB( + "pin_pitch", + [ + "Row Spacing", + "Pitch", + ], + ), + MappingParameterDB( + "spacer_height", + ["Insulation Height"], + ), + MappingParameterDB( + "pin_count_horizonal", + pin_count_source, + transform_fn=pin_structure_to_pin_count(vertical=False), + ), + MappingParameterDB( + "pin_count_vertical", + pin_count_source, + transform_fn=pin_structure_to_pin_count(vertical=True), + ), + ] + ( + ComponentQuery() + .filter_by_category("Connectors", pin_type) + .filter_by_package(pad_type) + .filter_by_stock(qty) + .filter_by_traits(cmp) + .sort_by_price(qty) + .filter_by_module_params_and_attach(cmp, mapping, qty) + ) + + # -------------------------------------------------------------------------------------- TYPE_SPECIFIC_LOOKUP = { @@ -508,4 +614,5 @@ def find_ldo(cmp: Module): F.Diode: find_diode, F.MOSFET: find_mosfet, F.LDO: find_ldo, + F.Header: find_header, } diff --git a/src/faebryk/libs/picker/lcsc.py b/src/faebryk/libs/picker/lcsc.py index 20b4d8c0..61e28a4e 100644 --- a/src/faebryk/libs/picker/lcsc.py +++ b/src/faebryk/libs/picker/lcsc.py @@ -203,8 +203,8 @@ def attach(component: Module, partno: str, get_model: bool = True): raise LCSC_PinmapException(partno, f"Failed to get pinmap: {e}") from e component.add(F.can_attach_to_footprint_via_pinmap(pinmap)) - sym = F.Symbol.with_component(component, pinmap) - sym.add(F.Symbol.has_kicad_symbol(f"lcsc:{easyeda_footprint.info.name}")) + sym = F.Symbol.with_component(component, pinmap) + sym.add(F.Symbol.has_kicad_symbol(f"lcsc:{easyeda_footprint.info.name}")) # footprint fp = F.KicadFootprint( diff --git a/test/libs/picker/test_jlcpcb.py b/test/libs/picker/test_jlcpcb.py index 7997221b..50dcdb53 100644 --- a/test/libs/picker/test_jlcpcb.py +++ b/test/libs/picker/test_jlcpcb.py @@ -356,6 +356,24 @@ def test_find_ldo(self): ], ) + def test_find_header(self): + self.TestRequirements( + self, + requirement=F.Header(horizonal_pin_count=3, vertical_pin_count=1).builder( + lambda h: ( + h.pad_type.merge(F.Constant(F.Header.PadType.THROUGH_HOLE)), + h.pin_type.merge(F.Constant(F.Header.PinType.MALE)), + h.angle.merge(F.Constant(F.Header.Angle.STRAIGHT)), + h.mating_pin_lenght.merge(F.Range.from_center_rel(6 * P.mm, 0.1)), + h.pin_pitch.merge(F.Constant(2.54 * P.mm)), + ) + ), + footprint=[ + ("HDR-TH_3P-P2.54-V-M", 3), + ("Plugin,P=2.54mm", 3), + ], + ) + def tearDown(self): # in test atexit not triggered, thus need to close DB manually JLCPCB_DB.get().close()