#!/usr/bin/env python3 """Summarise what a routed board actually contains. DRC answers "is anything wrong"; this answers "is anything missing, and does the copper match the rules it was routed under". It reports the still-open connections per net, the track width actually used per net class (the Specctra round trip is where widths silently drift), the layer split, and the via count. """ from __future__ import annotations import argparse from collections import Counter, defaultdict from pathlib import Path import pcbnew def net_groups(board: pcbnew.BOARD, net: str) -> int: """Count the separate copper islands a net is currently split into.""" connectivity = board.GetConnectivity() pads = [pad for fp in board.GetFootprints() for pad in fp.Pads() if pad.GetNetname() == net] seen: set[str] = set() groups = 0 for pad in pads: key = pad.m_Uuid.AsString() if key in seen: continue members = [item for item in connectivity.GetConnectedItems(pad, 0) if item.GetClass() == "PAD"] for item in members: seen.add(item.m_Uuid.AsString()) seen.add(key) groups += 1 return groups def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("board", type=Path) args = parser.parse_args() board = pcbnew.LoadBoard(str(args.board)) board.BuildConnectivity() net_class: dict[str, str] = {} pad_count: Counter[str] = Counter() for footprint in board.GetFootprints(): for pad in footprint.Pads(): name = pad.GetNetname() if name: net_class[name] = pad.GetNetClassName() pad_count[name] += 1 widths: dict[str, Counter[float]] = defaultdict(Counter) layers: Counter[str] = Counter() vias = 0 for item in board.GetTracks(): if isinstance(item, pcbnew.PCB_VIA): vias += 1 continue name = item.GetNetname() widths[net_class.get(name, "?")][round(pcbnew.ToMM(item.GetWidth()), 3)] += 1 layers[board.GetLayerName(item.GetLayer())] += 1 print(f"Board: {args.board}") print(f"Unconnected endpoints reported by connectivity: " f"{board.GetConnectivity().GetUnconnectedCount(True)}") print(f"Tracks: {sum(layers.values())} Vias: {vias}") print("\nTrack segments per layer:") for layer, count in layers.most_common(): print(f" {layer:10s} {count:6d}") print("\nTrack width used per net class (mm: segments):") for klass in sorted(widths): used = ", ".join(f"{width}: {count}" for width, count in sorted(widths[klass].items())) print(f" {klass:12s} {used}") print("\nNets still split into more than one copper group:") incomplete = [] for name in sorted(net_class): if pad_count[name] < 2: continue groups = net_groups(board, name) if groups > 1: incomplete.append((groups, name)) for groups, name in sorted(incomplete, reverse=True): print(f" {name:22s} {net_class[name]:12s} " f"{pad_count[name]:3d} pads in {groups} groups") print(f" ({len(incomplete)} nets incomplete)" if incomplete else " none - every net is one connected group") if __name__ == "__main__": main()