170 lines
5.3 KiB
Python
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
|