Files
2025-08-28 03:07:33 +07:00

170 lines
5.3 KiB
Python

"""
kicad_testpoints
Command line tool which exports the position of pads from a PCB to create a test point document
"""
import csv
import logging
from pathlib import Path
import pcbnew
_log = logging.getLogger("kicad_testpoints")
def calc_probe_distance(a, b):
dist = [a["x"] - b["x"], a["y"] - b["y"]]
return (dist[0] ** 2 + dist[1] ** 2) ** 0.5
def calc_probe_distances(name, probes_df):
"""
Calculate distance to all probes to this one. Return dict of distances.
"""
distances = {}
probe = probes_df[probes_df["test point ref des"] == name].iloc[0]
for _, line in probes_df.iterrows():
distances[line["test point ref des"]] = calc_probe_distance(probe, line)
return distances
class Settings:
"""
All the options that can be passed
"""
def __init__(self):
self.use_aux_origin: bool = False
def get_pad_side(p: pcbnew.PAD, **kwargs):
"""
As footprints can be on the top or bottom and the pad position is relative
to the footprint we need to use both the footprint and the pad position to get
the correct side. As the top layer/side is 0 then we can do the following.
"""
fp: pcbnew.FOOTPRINT = p.GetParentFootprint()
return "BOTTOM" if (fp.GetSide() - p.GetLayer()) else "TOP"
def calc_pad_position(center: tuple[float, float], origin: tuple[float, float]):
"""
Calculate pad position as relative to the origin and in cartesian coordinates.
The origin and center should be in native kicad pixel coordinates.
"""
return (center[0] - origin[0]), -1 * (center[1] - origin[1])
def get_pad_position(p: pcbnew.PAD, settings: Settings) -> tuple[float, float]:
"""
Get the center of the pad, the origin setting, and the quadrant setting,
calculate the transformed position.
The position internal to kicad never changes. The position is always the distance from
the top left with x increasing to the right and y increasing down.
Take the origin location and calculate the distance. Then multiple the axis so it is
increasing in the desired direction. To match the gerbers this should be increasing right and up.
"""
board = p.GetBoard()
ds = board.GetDesignSettings()
origin = (0, 0)
if settings.use_aux_origin:
origin = pcbnew.ToMM(ds.GetAuxOrigin())
center = [round(pt, 4) for pt in pcbnew.ToMM(p.GetCenter())]
position = calc_pad_position(origin=origin, center=center)
return [round(pt, 4) for pt in position]
def get_net_name(p: pcbnew.PAD, **kwargs):
"""
Get the identifier for connecting pads. Uses the short name which can cause conflicts.
"""
net = p.GetNetname()
return net
# Table of fields and how to get them
_fields = {
"source ref des": (
lambda p, **kwargs: p.GetParentFootprint().GetReferenceAsString()
),
"source pad": (lambda p, **kwargs: p.GetNumber()),
"net": get_net_name,
"net class": (lambda p, **kwargs: p.GetNetClassName()),
"side": get_pad_side,
"x": (lambda p, **kwargs: get_pad_position(p, **kwargs)[0]),
"y": (lambda p, **kwargs: get_pad_position(p, **kwargs)[1]),
"pad type": (lambda p, **kwargs: "THRU" if p.HasHole() else "SMT"),
"footprint side": (
lambda p, **kwargs: "BOTTOM" if p.GetParentFootprint().GetSide() else "TOP"
),
}
def write_csv(data: list[dict], filename: Path):
fieldnames = data[0].keys()
with filename.open("w", newline="") as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=fieldnames, delimiter=',', quotechar='"', quoting=csv.QUOTE_NONNUMERIC)
if fieldnames is not None:
writer.writeheader()
writer.writerows(data)
def build_test_point_report(
board: pcbnew.BOARD, settings: Settings, pads: tuple[pcbnew.PAD]
) -> list[dict]:
if settings.use_aux_origin:
ds = board.GetDesignSettings()
aux_origin = ds.GetAuxOrigin()
if aux_origin is None:
# Keep origin as 0,0
_log.info("No aux origin returned. Using 0,0 as origin")
settings.use_aux_origin = False
if pads:
assert isinstance(pads[0], pcbnew.PAD)
return [
{key: value(p, settings=settings) for key, value in _fields.items()}
for p in pads
]
def get_pads(
pad_pair: tuple[tuple[str, int]], board: pcbnew.BOARD
) -> tuple[pcbnew.PAD]:
"""
Get list of matching pads from a list of (ref_des, pad_num)
"""
pads = []
for ref_des, pad_number in pad_pair:
module = board.FindFootprintByReference(ref_des)
if not module:
msg = "Ref Des %s not found"
raise UserWarning(msg, ref_des)
found = False
for pad in module.Pads():
if str(pad.GetNumber()) == str(pad_number):
pads.append(pad)
found = True
break
if found is False:
nums = [str(pad.GetNumber()) for pad in module.Pads()]
msg = f"Pad {pad_number} not found in module {ref_des} ({nums})"
raise UserWarning(msg)
return pads
def get_pads_by_property(board: pcbnew.BOARD) -> tuple[pcbnew.PAD]:
"""
Get list of matching pads from a list of (ref_des, pad_num)
"""
test_point_property = 4
pads = []
for p in board.GetPads():
if p.GetProperty() != test_point_property:
continue
pads.append(p)
return pads