Match exposed pads by land, cite ESD, follow CC to Rd (2.84.0).

PE-BOM-011 treats EP/EPAD/THERMAL PAD and the footprint exposed land as one pad when both sides have one. PE-ESD-001 quotes a datasheet only with part and section, and does not warn on a 3V3 jack rail, uncitable VBUS, or optical S/PDIF. CC Rd is the packed 5.1 kΩ on the Type-C CC pin net. Ethernet MDI pair Z is PASS, FAIL, or ERROR against the cited 10/100 or gigabit clause.
This commit is contained in:
2026-09-22 21:56:09 +02:00
parent 7e872cfbc3
commit f79c722e68
6 changed files with 923 additions and 34 deletions
+371
View File
@@ -0,0 +1,371 @@
"""HubAudio false findings: exposed-pad names, ESD cites, CC-via-net, Ethernet Z."""
from __future__ import annotations
import pytest
from backend.periscopex.af_trace_check import check_af_traces
from backend.periscopex.bom_pcb_check import check_bom_pcb_datasheet
from backend.periscopex.esd_return_check import check_esd
from backend.periscopex.models import (
Component,
ComponentConstraints,
ComponentType,
DesignGraph,
LayoutFootprint,
LayoutGraph,
LayoutPad,
LayoutSegment,
Net,
NetType,
PackageInfo,
Pin,
PinConnection,
)
from backend.periscopex.protocol_l0 import _run_l0_check, l0_findings
from backend.periscopex.protocol_recognize import PhysicalBusInstance
def _ic(ref: str, mpn: str, pins: dict[str, str], *, footprint: str = "") -> Component:
return Component(
reference=ref, value=mpn, footprint=footprint,
component_type=ComponentType.IC, mpn=mpn, pins=pins,
)
def _net(name: str, *pairs: tuple[str, str]) -> Net:
return Net(
name=name, net_type=NetType.SIGNAL,
pins=[PinConnection(component_ref=r, pin_number=p) for r, p in pairs],
)
def _cons(mpn: str, numbers: list[str], pin_count: int) -> ComponentConstraints:
return ComponentConstraints(
mpn=mpn,
package_info=PackageInfo(base_family="QFN", package=f"QFN-{pin_count}", pin_count=pin_count),
pintable=[Pin(number=n, name=n) for n in numbers],
absolute_maximum_ratings=[],
rules=[],
)
def _layout(ref: str, pads: list[tuple[str, str]], *, footprint: str) -> LayoutGraph:
return LayoutGraph(
footprints={
ref: LayoutFootprint(
reference=ref, footprint=footprint, x=0, y=0,
pads=[
LayoutPad(number=n, x=0, y=0, net="GND", pinfunction=pf)
for n, pf in pads
],
),
},
)
def _bom(ref: str, mpn: str, numbers: list[str], pads: list[tuple[str, str]], pin_count: int):
graph = DesignGraph(components={
ref: _ic(ref, mpn, {n: "N" for n, _pf in pads if n}, footprint="Lib:Part"),
})
# Schematic pins follow the pintable numbers that are real contacts.
graph.components[ref].pins = {n: "N" for n in numbers if n not in {"EP", "THERMAL PAD"}}
layout = _layout(ref, pads, footprint="Lib:Part")
findings = check_bom_pcb_datasheet(graph, {mpn: _cons(mpn, numbers, pin_count)}, layout)
return [f for f in findings if f.rule_id == "PE-BOM-011"]
def test_u16_ep_matches_pad_41():
"""AXP2101: pintable EP, footprint pad 41 / EP_41."""
hits = _bom(
"U16", "AXP2101", ["1", "EP"],
[("1", "CHGLED_1"), ("41", "EP_41")],
40,
)
assert hits == []
def test_u18_ep_matches_pad_89():
"""ADAU1467: pintable EP, footprint pad 89 / EP_89."""
hits = _bom(
"U18", "ADAU1467", ["1", "EP"],
[("1", "DGND_1_1"), ("89", "EP_89")],
88,
)
assert hits == []
def test_u19_ep_matches_pad_25_vss():
"""LAN8720A: pintable EP, VQFN-24 land is pad 25 / VSS_25."""
hits = _bom(
"U19", "LAN8720A", ["1", "EP"],
[("1", "VDD2A_1"), ("25", "VSS_25")],
24,
)
assert hits == []
def test_u5_thermal_pad_matches_epad_29():
"""TAC5212: pintable THERMAL PAD, footprint pad 29 / EPAD_29."""
hits = _bom(
"U5", "XTAC5212", ["1", "THERMAL PAD"],
[("1", "DREG_1"), ("29", "EPAD_29")],
24,
)
assert hits == []
def test_exposed_pad_absent_from_footprint_stays_error():
hits = _bom(
"U9", "BARE", ["1", "EP"],
[("1", "PIN_1")],
24,
)
assert hits
assert any("EP" in (f.finding or "") for f in hits)
def _esd_graph(net: str, *, j_fp: str = "", j_value: str = "JACK") -> DesignGraph:
return DesignGraph(
components={
"J1": Component(
reference="J1", value=j_value, footprint=j_fp,
component_type=ComponentType.CONNECTOR, mpn="",
pins={"13": net},
),
"U19": _ic("U19", "LAN8720A", {"1": net}),
},
nets={net: _net(net, ("J1", "13"), ("U19", "1"))},
)
def test_esd_does_not_say_datasheet_specified_without_a_cite():
findings = check_esd(_esd_graph("USB_DP"))
assert findings and findings[0].rule_id == "PE-ESD-001"
blob = f"{findings[0].action} {findings[0].recommendation}"
assert "datasheet-specified" not in blob
def test_esd_quotes_datasheet_when_part_and_section_exist():
cons = ComponentConstraints(
mpn="LAN8720A",
pintable=[],
absolute_maximum_ratings=[],
rules=[],
layout_rules=[{
"kind": "esd",
"document": "LAN8720A datasheet",
"section": "3.2",
"page": "12",
"note": "ESD on USB_DP",
}],
)
findings = check_esd(_esd_graph("USB_DP"), {"LAN8720A": cons})
assert findings
blob = f"{findings[0].action} {findings[0].finding}"
assert "datasheet-specified" in blob
assert "LAN8720A datasheet" in blob
assert "3.2" in blob
def test_3v3_ethernet_rail_is_not_an_esd_warning():
assert check_esd(_esd_graph("3V3_ETHERNET")) == []
def test_vbus_without_cite_is_not_an_esd_warning():
graph = _esd_graph("VBUS")
graph.components["U16"] = _ic("U16", "AXP2101", {"37": "VBUS"})
graph.components["J2"] = Component(
reference="J2", value="USB_C", footprint="Connector_USB:USB_C_Receptacle",
component_type=ComponentType.CONNECTOR,
pins={"A4": "VBUS"},
)
graph.nets = {"VBUS": _net("VBUS", ("J2", "A4"), ("U16", "37"))}
findings = check_esd(graph)
assert findings == []
blob = " ".join(f.action or "" for f in findings)
assert "datasheet-specified" not in blob
def test_optical_spdif_is_not_an_esd_warning():
findings = check_esd(_esd_graph(
"SPIF_IN",
j_fp="OptoDevice:Broadcom_AFBR-16xxZ_Horizontal",
j_value="TOSLINK",
))
assert findings == []
def test_unverified_spdif_warns_it_was_not_seen_as_optical():
findings = check_esd(_esd_graph("SPIF_OUT", j_fp="Connector:PinHeader"))
assert findings
assert findings[0].status == "WARNING"
assert "not verified as optical" in (findings[0].finding or "")
assert "datasheet-specified" not in (findings[0].action or "")
def _cc_inst() -> PhysicalBusInstance:
return PhysicalBusInstance(
instance_id="usb-c-usb2-receptacle:J2",
logical_protocol_id="usb2-hs",
physical_interface_id="usb-c-usb2-receptacle",
pcb_relevant="YES",
confidence=1.0,
evidence_kind="pinout",
recognition_status="RECOGNIZED",
host_ref="J2",
nets=["Net-(J2-A5)", "Net-(J2-B5)"],
)
def _cc_graph(*, both: bool) -> DesignGraph:
comps = {
"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,
pins={"A5": "Net-(J2-A5)", "B5": "Net-(J2-B5)", "A4": "VBUS"},
),
"R1": Component(
reference="R1", value="5.1k", footprint="R_0603",
component_type=ComponentType.RESISTOR,
pins={"1": "Net-(J2-A5)", "2": "GND"},
),
}
nets = {
"Net-(J2-A5)": _net("Net-(J2-A5)", ("J2", "A5"), ("R1", "1")),
"Net-(J2-B5)": _net("Net-(J2-B5)", ("J2", "B5")),
"GND": _net("GND", ("R1", "2")),
}
if both:
comps["R2"] = Component(
reference="R2", value="5.1k", footprint="R_0603",
component_type=ComponentType.RESISTOR,
pins={"1": "Net-(J2-B5)", "2": "GND"},
)
nets["Net-(J2-B5)"] = _net("Net-(J2-B5)", ("J2", "B5"), ("R2", "1"))
nets["GND"] = _net("GND", ("R1", "2"), ("R2", "2"))
return DesignGraph(components=comps, nets=nets)
def test_cc_rd_present_on_unnamed_nets_does_not_fail():
row = _run_l0_check(_cc_graph(both=True), _cc_inst(), "cc_rd_rp", "MANDATORY")
assert row.result == "PASS"
assert "5.1" in row.notes
assert "CC1/CC2 net not present" not in row.notes
findings = l0_findings(_cc_inst(), [row])
assert not any(f.rule_id == "PE-PRT-L0-001" for f in findings)
def test_cc_rd_missing_on_pin_net_fails():
row = _run_l0_check(_cc_graph(both=False), _cc_inst(), "cc_rd_rp", "MANDATORY")
assert row.result == "FAIL"
assert "B5" in row.notes
assert "CC1/CC2 net not present" not in row.notes
assert "5.1" in row.notes
findings = l0_findings(_cc_inst(), [row])
assert any(f.rule_id == "PE-PRT-L0-001" and f.status == "ERROR" for f in findings)
def _eth_graph(mpn: str, *, value: str = "") -> DesignGraph:
return DesignGraph(
components={
"J1": Component(
reference="J1", value="RJ45",
footprint="Connector_RJ:RJ45_Abracon_ARJP11A-MA_Horizontal",
component_type=ComponentType.CONNECTOR,
pins={"9": "RJ45_TXP", "10": "RJ45_TXN"},
),
"U19": _ic("U19", mpn, {"21": "RJ45_TXP", "20": "RJ45_TXN"}, footprint="VQFN-24"),
},
nets={
"RJ45_TXP": _net("RJ45_TXP", ("J1", "9"), ("U19", "21")),
"RJ45_TXN": _net("RJ45_TXN", ("J1", "10"), ("U19", "20")),
},
) if not value else DesignGraph(
components={
"J1": Component(
reference="J1", value="RJ45",
footprint="Connector_RJ:RJ45",
component_type=ComponentType.CONNECTOR,
pins={"9": "RJ45_TXP", "10": "RJ45_TXN"},
),
"U19": Component(
reference="U19", value=value, footprint="",
component_type=ComponentType.IC, mpn=mpn,
pins={"21": "RJ45_TXP", "20": "RJ45_TXN"},
),
},
nets={
"RJ45_TXP": _net("RJ45_TXP", ("J1", "9"), ("U19", "21")),
"RJ45_TXN": _net("RJ45_TXN", ("J1", "10"), ("U19", "20")),
},
)
def _eth_layout() -> LayoutGraph:
return LayoutGraph(
segments=[
LayoutSegment(start=(0, 0), end=(20, 0), width=0.2, layer="F.Cu", net="RJ45_TXP"),
LayoutSegment(start=(0, 0.2), end=(20, 0.2), width=0.2, layer="F.Cu", net="RJ45_TXN"),
],
)
def _eth_findings(graph: DesignGraph, monkeypatch, z: float | None):
if z is not None:
monkeypatch.setattr(
"backend.periscopex.af_trace_check._pair_z_ohm",
lambda layout, unit: z,
)
return [f for f in check_af_traces(graph, {}, _eth_layout()) if f.rule_id == "PE-AF-002"]
def test_lan8720_pair_without_z_is_error_with_100base_cite(monkeypatch):
findings = _eth_findings(_eth_graph("LAN8720A"), monkeypatch, None)
assert len(findings) == 1
f = findings[0]
assert f.status == "ERROR"
blob = f"{f.finding} {f.action} {f.requirement}"
assert blob.startswith("ERROR:") or "ERROR:" in f.finding
assert "100" in blob and "25.4.9" in blob
assert "stackup" in blob
assert "Fornire" not in blob
assert "tr" not in blob.lower()
assert "90" not in blob
assert "Clause 40" not in blob
assert "1000BASE" not in blob
assert "PASS" not in f.finding
def test_lan8720_pair_z_inside_cite_is_pass(monkeypatch):
findings = _eth_findings(_eth_graph("LAN8720A"), monkeypatch, 100.0)
assert len(findings) == 1
assert findings[0].finding.startswith("PASS:")
assert findings[0].status == "INFO"
assert "RJ45_TXP" in findings[0].finding and "RJ45_TXN" in findings[0].finding
assert "25.4.9" in findings[0].finding
def test_lan8720_pair_z_outside_cite_is_fail(monkeypatch):
findings = _eth_findings(_eth_graph("LAN8720A"), monkeypatch, 90.0)
assert len(findings) == 1
assert findings[0].finding.startswith("FAIL:")
assert findings[0].status == "ERROR"
assert "PASS" not in findings[0].finding
assert "25.4.9" in findings[0].finding
def test_gigabit_phy_uses_clause_40_not_100base(monkeypatch):
findings = _eth_findings(
_eth_graph("RTL8211F", value="1000BASE-T"),
monkeypatch,
None,
)
assert len(findings) == 1
blob = findings[0].finding
assert findings[0].status == "ERROR"
assert "Clause 40" in blob
assert "25.4.9" not in blob
assert "PASS" not in blob