"""PE-BOM-011 joins pintable × lib × PCB by name. pin_count is not authority.""" from __future__ import annotations from pathlib import Path from backend.periscopex.bom_pcb_check import ( _is_ep_land, check_bom_pcb_datasheet, ) from backend.periscopex.models import ( Component, ComponentConstraints, ComponentType, DesignGraph, LayoutFootprint, LayoutGraph, LayoutPad, PackageInfo, Pin, ) from backend.periscopex.parsers_kicad_pcb import parse_kicad_pcb _HUB_PCB = Path( "/Users/michelebigi/Development/HubAudio/hardware/kicad/HubAudio/HubAudio.kicad_pcb" ) _USB_C_PINS = [ "A1", "A4", "A5", "A6", "A7", "A8", "A9", "A12", "B1", "B4", "B5", "B6", "B7", "B8", "B9", "B12", ] def _usb_c_cons(*, pintable: bool) -> ComponentConstraints: pins = [Pin(number=n, name=n) for n in _USB_C_PINS] if pintable else [] return ComponentConstraints( mpn="TYPE-C-31-M-12", package_info=PackageInfo( base_family="USB", package="USB-C-16P", pin_count=16, ), pintable=pins, absolute_maximum_ratings=[], rules=[], ) def _j2_graph(pins: dict[str, str] | None = None) -> DesignGraph: keys = pins or {n: "SIG" for n in _USB_C_PINS} keys.setdefault("SH", "GND") return DesignGraph( components={ "J2": Component( reference="J2", value="USB_C_Receptacle_USB2.0_16P", footprint="Connector_USB:USB_C_Receptacle_HRO_TYPE-C-31-M-12", component_type=ComponentType.CONNECTOR, mpn="TYPE-C-31-M-12", pins=keys, ), }, ) def _j2_layout() -> LayoutGraph: pads = [ LayoutPad(number=n, x=0, y=0, net="SIG", pinfunction=f"PIN_{n}") for n in _USB_C_PINS ] pads.extend( LayoutPad(number="SH", x=i, y=0, net="GND", pinfunction="SHIELD_SH") for i in range(4) ) return LayoutGraph( footprints={ "J2": LayoutFootprint( reference="J2", footprint="Connector_USB:USB_C_Receptacle_HRO_TYPE-C-31-M-12", x=0, y=0, layer="F.Cu", pads=pads, ), }, ) def _ids(findings) -> set[str]: return {f.rule_id for f in findings} def test_a5_b5_are_not_ep(): assert _is_ep_land("A5", "CC1_A5", pin_count=16) is False assert _is_ep_land("B5", "CC2_B5", pin_count=16) is False assert _is_ep_land("EP", pin_count=8) is True assert _is_ep_land("9", "EP", pin_count=8) is True assert _is_ep_land("9", pin_count=8) is True assert _is_ep_land("9_1", pin_count=8) is True def test_usb_c_pin_count_16_without_pintable_is_not_error(): """The 16 vs 1 SH false ERROR: pin_count alone must not fire PE-BOM-011.""" findings = check_bom_pcb_datasheet( _j2_graph(), {"TYPE-C-31-M-12": _usb_c_cons(pintable=False)}, _j2_layout(), ) assert "PE-BOM-011" not in _ids(findings) assert not any("pin_count=16" in (f.finding or "") for f in findings) def test_usb_c_pintable_matches_contacts_ignores_sh_and_mount(): findings = check_bom_pcb_datasheet( _j2_graph(), {"TYPE-C-31-M-12": _usb_c_cons(pintable=True)}, _j2_layout(), ) assert "PE-BOM-011" not in _ids(findings) def test_hubaudio_j2_is_not_16_vs_1_signal_pad(): if not _HUB_PCB.is_file(): return layout = parse_kicad_pcb(_HUB_PCB) j2 = layout.footprints.get("J2") assert j2 is not None numbers = {p.number for p in j2.pads} assert "A5" in numbers and "B5" in numbers assert "SH" in numbers slim = LayoutGraph(footprints={"J2": j2}) findings = check_bom_pcb_datasheet( _j2_graph(), {"TYPE-C-31-M-12": _usb_c_cons(pintable=False)}, slim, ) bom011 = [f for f in findings if f.rule_id == "PE-BOM-011"] assert bom011 == [] findings_named = check_bom_pcb_datasheet( _j2_graph(), {"TYPE-C-31-M-12": _usb_c_cons(pintable=True)}, slim, ) assert "PE-BOM-011" not in _ids(findings_named) def test_qfn_ep_still_excluded_from_join(): cons = ComponentConstraints( mpn="SW", package_info=PackageInfo(base_family="TI", package="SON-8-EP", pin_count=8), pintable=[Pin(number=str(i), name=f"P{i}") for i in range(1, 9)], absolute_maximum_ratings=[], rules=[], ) pads = [LayoutPad(number=str(i), x=0, y=0, net="GND") for i in range(1, 9)] pads += [ LayoutPad(number="9", x=0, y=0, net="GND", pinfunction="EP"), LayoutPad(number="9_1", x=0.2, y=0, net="GND"), LayoutPad(number="9_2", x=0.4, y=0, net="GND"), ] graph = DesignGraph( components={ "U1": Component( reference="U1", value="SW", footprint="SON-8", component_type=ComponentType.IC, mpn="SW", pins={str(i): "GND" for i in range(1, 9)}, ), }, ) layout = LayoutGraph( footprints={"U1": LayoutFootprint(reference="U1", x=0, y=0, pads=pads)}, ) assert "PE-BOM-011" not in _ids( check_bom_pcb_datasheet(graph, {"SW": cons}, layout), ) def test_missing_signal_pad_still_fires(): cons = ComponentConstraints( mpn="IC24", package_info=PackageInfo(base_family="QFN", package="QFN-24", pin_count=24), pintable=[Pin(number=str(i), name=f"P{i}") for i in range(1, 25)], absolute_maximum_ratings=[], rules=[], ) pads = [LayoutPad(number=str(i), x=0, y=0, net="SIG") for i in range(1, 24)] graph = DesignGraph( components={ "U1": Component( reference="U1", value="IC", footprint="QFN-24", component_type=ComponentType.IC, mpn="IC24", pins={str(i): "SIG" for i in range(1, 25)}, ), }, ) layout = LayoutGraph( footprints={"U1": LayoutFootprint(reference="U1", x=0, y=0, pads=pads)}, ) findings = check_bom_pcb_datasheet(graph, {"IC24": cons}, layout) assert "PE-BOM-011" in _ids(findings) assert any("24" in (f.finding or "") for f in findings if f.rule_id == "PE-BOM-011") def _write_mod(root: Path, lib: str, name: str, pads: list[str]) -> Path: pretty = root / f"{lib}.pretty" pretty.mkdir(parents=True, exist_ok=True) body = [f'(footprint "{lib}:{name}" (version 20240108) (layer "F.Cu")\n'] for i, n in enumerate(pads): body.append( f' (pad "{n}" smd rect (at {i} 0) (size 0.3 0.8) ' f'(layers "F.Cu" "F.Paste" "F.Mask") (pinfunction "{n}"))\n' ) body.append(")\n") path = pretty / f"{name}.kicad_mod" path.write_text("".join(body)) return path def test_lib_vs_datasheet_is_lib_finding_not_pcb_pad_count(tmp_path: Path): _write_mod(tmp_path, "Pkg", "QFN-8", [str(i) for i in range(1, 8)]) cons = ComponentConstraints( mpn="IC8", package_info=PackageInfo(base_family="QFN", package="QFN-8", pin_count=8), pintable=[Pin(number=str(i), name=f"P{i}") for i in range(1, 9)], absolute_maximum_ratings=[], rules=[], ) pads = [LayoutPad(number=str(i), x=0, y=0, net="SIG") for i in range(1, 9)] graph = DesignGraph( components={ "U1": Component( reference="U1", value="IC", footprint="Pkg:QFN-8", component_type=ComponentType.IC, mpn="IC8", pins={str(i): "SIG" for i in range(1, 9)}, ), }, ) layout = LayoutGraph( footprints={ "U1": LayoutFootprint( reference="U1", footprint="Pkg:QFN-8", x=0, y=0, pads=pads, ), }, ) findings = [ f for f in check_bom_pcb_datasheet( graph, {"IC8": cons}, layout, footprint_dirs=[tmp_path], ) if f.rule_id == "PE-BOM-011" ] assert findings text = " ".join(f.finding for f in findings) assert "library" in text.lower() assert "pin_count=" not in text assert "signal pads=" not in text def test_lib_vs_embedded_pcb_is_separate_finding(tmp_path: Path): _write_mod(tmp_path, "Pkg", "QFN-8", [str(i) for i in range(1, 9)]) cons = ComponentConstraints( mpn="IC8", package_info=PackageInfo(base_family="QFN", package="QFN-8", pin_count=8), pintable=[Pin(number=str(i), name=f"P{i}") for i in range(1, 9)], absolute_maximum_ratings=[], rules=[], ) pads = [LayoutPad(number=str(i), x=0, y=0, net="SIG") for i in range(1, 8)] pads.append(LayoutPad(number="99", x=1, y=0, net="SIG")) graph = DesignGraph( components={ "U1": Component( reference="U1", value="IC", footprint="Pkg:QFN-8", component_type=ComponentType.IC, mpn="IC8", pins={str(i): "SIG" for i in range(1, 9)}, ), }, ) layout = LayoutGraph( footprints={ "U1": LayoutFootprint( reference="U1", footprint="Pkg:QFN-8", x=0, y=0, pads=pads, ), }, ) findings = [ f for f in check_bom_pcb_datasheet( graph, {"IC8": cons}, layout, footprint_dirs=[tmp_path], ) if f.rule_id == "PE-BOM-011" ] assert any("embedded" in f.finding.lower() or "PCB" in f.finding for f in findings)