Pad-net compare strips leading slash and nested sheet path so /ESP32_EN matches ESP32_EN. If every mismatch is only that prefix, emit PE-LAY-003 once instead of per-pad reconnect errors.
181 lines
6.2 KiB
Python
181 lines
6.2 KiB
Python
"""Pad net on the PCB vs pin net on the schematic graph.
|
|
|
|
Silent when either side has no name. Power-flag footprints (#PWR) skipped.
|
|
|
|
KiCad PCB net names often keep a root-sheet ``/`` (and nested sheet path)
|
|
that a flattened schematic netlist omits. Those are the same net: do not
|
|
emit per-pad reconnect findings.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from backend.periscopex.models import DesignGraph, Finding, LayoutGraph
|
|
|
|
|
|
def normalize_kicad_hierarchy_net(name: str) -> str:
|
|
"""Drop leading slashes; keep inner sheet path (``sheet/NET``)."""
|
|
n = (name or "").strip().replace("\\", "/")
|
|
while n.startswith("/"):
|
|
n = n[1:]
|
|
while n.endswith("/") and n:
|
|
n = n[:-1]
|
|
return n
|
|
|
|
|
|
def kicad_nets_match(a: str, b: str) -> bool:
|
|
"""True if schematic and PCB names are the same KiCad net.
|
|
|
|
Equal strings match. After stripping leading ``/``, equal names match
|
|
(``/ESP32_EN`` vs ``ESP32_EN``). A flattened leaf also matches a nested
|
|
path that ends with ``/{leaf}`` (``REGN`` vs ``/power/REGN``). Different
|
|
sheets that only share a last segment (``sheet1/CLK`` vs ``sheet2/CLK``)
|
|
do not match each other.
|
|
"""
|
|
if not a or not b:
|
|
return False
|
|
if a == b:
|
|
return True
|
|
na = normalize_kicad_hierarchy_net(a)
|
|
nb = normalize_kicad_hierarchy_net(b)
|
|
if not na or not nb:
|
|
return False
|
|
if na == nb:
|
|
return True
|
|
return na.endswith("/" + nb) or nb.endswith("/" + na)
|
|
|
|
|
|
def _ensure_recommendation(f: Finding) -> Finding:
|
|
if not (f.recommendation or "").strip():
|
|
f.recommendation = (
|
|
"Align the PCB pad net with the schematic pin, then re-run PCB review."
|
|
)
|
|
return f
|
|
|
|
|
|
def _hierarchy_only_mismatch(pcb_net: str, sch_net: str) -> bool:
|
|
"""PCB vs schematic differ only by KiCad sheet-path prefix."""
|
|
if pcb_net == sch_net:
|
|
return False
|
|
return kicad_nets_match(pcb_net, sch_net)
|
|
|
|
|
|
def check_pcb_net_match(
|
|
graph: DesignGraph,
|
|
layout: LayoutGraph | None,
|
|
) -> list[Finding]:
|
|
if layout is None or not layout.footprints:
|
|
return []
|
|
findings: list[Finding] = []
|
|
sch_refs = {
|
|
ref for ref in graph.components
|
|
if not ref.startswith("#")
|
|
}
|
|
pcb_refs = {
|
|
ref for ref in layout.footprints
|
|
if not ref.startswith("#")
|
|
}
|
|
|
|
for ref in sorted(sch_refs - pcb_refs):
|
|
comp = graph.components[ref]
|
|
findings.append(_ensure_recommendation(Finding(
|
|
designator=ref,
|
|
mpn=comp.mpn or "",
|
|
aspect="pcb_match",
|
|
finding=f"{ref} is in the schematic but has no footprint on the PCB.",
|
|
why="Layout review cannot check a part that is not placed.",
|
|
status="WARNING",
|
|
recommendation=f"Place {ref} on the board or remove it from the schematic/BOM.",
|
|
source="pcb_net_match",
|
|
rule_id="PE-LAY-002",
|
|
pins=[],
|
|
)))
|
|
|
|
real: list[tuple[str, str, str, str]] = [] # ref, pad, pcb_net, sch_net
|
|
hierarchy: list[tuple[str, str, str, str]] = []
|
|
|
|
for ref, fp in sorted(layout.footprints.items()):
|
|
if ref.startswith("#"):
|
|
continue
|
|
comp = graph.components.get(ref)
|
|
if comp is None:
|
|
continue
|
|
for pad in fp.pads:
|
|
if not pad.net or not pad.number:
|
|
continue
|
|
sch_net = graph.pin_net(ref, pad.number)
|
|
if not sch_net:
|
|
continue
|
|
if pad.net == sch_net:
|
|
continue
|
|
if _hierarchy_only_mismatch(pad.net, sch_net):
|
|
hierarchy.append((ref, pad.number, pad.net, sch_net))
|
|
continue
|
|
real.append((ref, pad.number, pad.net, sch_net))
|
|
|
|
if hierarchy and not real:
|
|
examples: list[str] = []
|
|
seen_pairs: set[tuple[str, str]] = set()
|
|
for _ref, _pad, pcb_net, sch_net in hierarchy:
|
|
pair = (pcb_net, sch_net)
|
|
if pair in seen_pairs:
|
|
continue
|
|
seen_pairs.add(pair)
|
|
examples.append(f"PCB '{pcb_net}' ↔ schematic '{sch_net}'")
|
|
if len(examples) >= 8:
|
|
break
|
|
extra = len(seen_pairs) - len(examples)
|
|
example_txt = "; ".join(examples)
|
|
if extra > 0:
|
|
example_txt += f" (+{extra} more)"
|
|
first_ref = hierarchy[0][0]
|
|
first_comp = graph.components.get(first_ref)
|
|
findings.append(_ensure_recommendation(Finding(
|
|
designator=first_ref,
|
|
mpn=(first_comp.mpn if first_comp else "") or "",
|
|
aspect="pcb_match",
|
|
finding=(
|
|
"Schematic and PCB net names differ only by KiCad hierarchy "
|
|
f"prefix ({len(hierarchy)} pads). Examples: {example_txt}."
|
|
),
|
|
why=(
|
|
"KiCad stores board nets with a leading '/' and optional sheet "
|
|
"path; a flattened schematic netlist usually omits that prefix. "
|
|
"These pads are the same electrical nets."
|
|
),
|
|
status="INFO",
|
|
recommendation=(
|
|
"Update the PCB from the schematic (or re-export a netlist with "
|
|
"the same hierarchy flattening) so names match. Do not retouch "
|
|
"individual pads."
|
|
),
|
|
source="pcb_net_match",
|
|
rule_id="PE-LAY-003",
|
|
pins=[],
|
|
)))
|
|
|
|
for ref, pad_no, pcb_net, sch_net in real:
|
|
comp = graph.components[ref]
|
|
findings.append(_ensure_recommendation(Finding(
|
|
designator=ref,
|
|
mpn=comp.mpn or "",
|
|
aspect="pcb_match",
|
|
finding=(
|
|
f"{ref}.{pad_no} PCB net '{pcb_net}' does not match "
|
|
f"schematic net '{sch_net}'."
|
|
),
|
|
why=(
|
|
"Datasheet and SI checks follow schematic net names. "
|
|
"A pad on the wrong net is a layout error, not a BOM typo."
|
|
),
|
|
status="ERROR",
|
|
recommendation=(
|
|
f"Reconnect pad {ref}.{pad_no} to '{sch_net}' "
|
|
f"(or fix the schematic if the PCB is authoritative)."
|
|
),
|
|
source="pcb_net_match",
|
|
rule_id="PE-LAY-001",
|
|
net=pcb_net,
|
|
pins=[pad_no],
|
|
)))
|
|
return findings
|