main
John Lauer Component record for ltc2945cms-pbf: files batch 3 16c3b96 23h ago
#!/usr/bin/env python3
"""Pad-by-pad comparison of the page footprint against Analog Devices' recommended solder pad layout for the MS12
package (LTC DWG 05-08-1668 Rev A, LTC2945 datasheet Rev. D p.32: pads 0.42 x 0.889 mm, pitch 0.65 BSC, inner gap
3.20 to 3.45 mm, outer extent 5.10 mm min) and against KiCad's MSOP-12_3x4.039mm_P0.65mm; draws the three with the
3D model's lead contact areas (section of the model at z = 0.01 mm) in KiCad's footprint frame (Y down).
usage: fp_check_ms12.py page.kicad_mod kicad.kicad_mod model.step out.json out.png   (run with the CadQuery python)"""
import re, sys, json
import cadquery as cq
from OCP.BRepAlgoAPI import BRepAlgoAPI_Section
from OCP.gp import gp_Pln, gp_Pnt, gp_Dir
import matplotlib; matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
page, kic, step, oj, op = sys.argv[1:6]

def pads(p):
    s = open(p).read(); out = {}
    for m in re.finditer(r'\(pad "(\d+)" smd (\w+)\s*\(at (\S+) (\S+)\)\s*\(size (\S+) (\S+)\)', s):
        n, sh, x, y, w, h = m.groups(); out[n] = dict(shape=sh, x=float(x), y=float(y), w=float(w), h=float(h))
    return out

E, X1, Y1 = 0.65, 0.42, 0.889
GAP = (3.20 + 3.45) / 2                     # inner edge to inner edge, middle of the printed range
XC = round(GAP / 2 + Y1 / 2, 4)             # pad centre, 2.1070
ys = [round(-2.5 * E + i * E, 4) for i in range(6)]
adi = {}
for i, y in enumerate(ys): adi[str(i + 1)] = dict(x=-XC, y=y, w=Y1, h=X1)
for i, y in enumerate(reversed(ys)): adi[str(i + 7)] = dict(x=XC, y=y, w=Y1, h=X1)
pp, kp = pads(page), pads(kic)
rows = []; ok = True
for n in sorted(adi, key=int):
    a, b, k = adi[n], pp[n], kp[n]
    d = {key: round(b[key] - a[key], 4) for key in ("x", "y", "w", "h")}
    match = all(abs(v) < 1e-6 for v in d.values()); ok &= match
    rows.append(dict(pad=n, adi=a, page=b, page_minus_adi=d, match=match, kicad=k,
                     kicad_minus_adi={key: round(k[key] - a[key], 4) for key in ("x", "y", "w", "h")}))
outer = round(2 * XC + Y1, 4)

shp = cq.Compound.makeCompound(cq.importers.importStep(step).solids().vals())
sec = BRepAlgoAPI_Section(shp.wrapped, gp_Pln(gp_Pnt(0, 0, 0.01), gp_Dir(0, 0, 1))); sec.Build()
edges = cq.Shape.cast(sec.Shape()).Edges()
leads = {}
for e in edges:
    bb = e.BoundingBox(); cx, cy = (bb.xmin + bb.xmax) / 2, -(bb.ymin + bb.ymax) / 2
    key = (1 if cx > 0 else -1, round((cy + 2.5 * E) / E))
    L = leads.setdefault(key, [9, 9, -9, -9])
    L[0] = min(L[0], bb.xmin); L[1] = min(L[1], -bb.ymax); L[2] = max(L[2], bb.xmax); L[3] = max(L[3], -bb.ymin)
contact = []
for (sx, k), (x0, y0, x1, y1) in sorted(leads.items()):
    contact.append(dict(side=sx, y_centre=round((y0 + y1) / 2, 4), x=[round(x0, 4), round(x1, 4)], width=round(y1 - y0, 4)))
inside = []
for c in contact:
    n = next(k for k, p in pp.items() if abs(p["y"] - c["y_centre"]) < 0.05 and (p["x"] > 0) == (c["side"] > 0))
    p = pp[n]
    inside.append(dict(pad=n, contact_x=c["x"], contact_width=c["width"], pad_x=[round(p["x"] - p["w"] / 2, 4), round(p["x"] + p["w"] / 2, 4)],
                       contact_inside_pad=bool(p["x"] - p["w"] / 2 - 1e-6 <= min(c["x"]) and max(c["x"]) <= p["x"] + p["w"] / 2 + 1e-6
                                                and c["width"] <= p["h"] + 1e-6)))

res = dict(schema="adom/footprint-check@1",
           reference="Analog Devices LTC2945 datasheet Rev. D p.32, MS Package 12-Lead Plastic MSOP, LTC DWG 05-08-1668 Rev A, RECOMMENDED SOLDER PAD LAYOUT",
           values_mm=dict(pitch=E, pad_width=X1, pad_length=Y1, inner_gap_printed=[3.20, 3.45], inner_gap_used=GAP,
                          pad_centre_x=XC, outer_extent=outer, outer_extent_printed_min=5.10),
           choice="The drawing prints the inner gap as a range (3.20 to 3.45 mm) and the outer extent as 5.10 mm minimum; the page uses the middle of the range, 3.325 mm, which gives an outer extent of 5.103 mm (meets the 5.10 minimum).",
           page_footprint=page.split("/")[-1], all_pads_match=ok, pads=rows,
           kicad_comparison=("KiCad MSOP-12_3x4.039mm_P0.65mm (IPC-7351 nominal, generated from the same LTC drawing 05-08-1668): "
                             "pads 1.45 x 0.40 mm roundrect 25 % at x = +-2.15; ADI's are 0.889 x 0.42 rectangles at x = +-2.107. "
                             "KiCad's pad is 0.561 mm longer and 0.02 mm narrower; its inner edge sits 1.425 mm from centre (ADI 1.6625, so 0.2375 mm further under the body) and its outer edge 2.875 mm (ADI 2.5515, 0.3235 mm further out). Pitch and pin order identical. The page follows ADI's drawing."),
           model_contact=dict(method="section of the 3D model at z = 0.01 mm, one bounding box per lead, flipped to footprint Y down",
                              leads=inside, all_inside=all(i["contact_inside_pad"] for i in inside)))
json.dump(res, open(oj, "w"), indent=1)

fig, ax = plt.subplots(figsize=(8, 7), dpi=120); ax.set_facecolor("white")
ax.add_patch(Rectangle((-1.5, -2.0195), 3.0, 4.039, fill=False, ec="#555", lw=1))
for n, p in pp.items():
    ax.add_patch(Rectangle((p["x"] - p["w"] / 2, p["y"] - p["h"] / 2), p["w"], p["h"], fc="#00b8b1", alpha=0.45, ec="#00807a"))
    ax.text(p["x"], p["y"], n, ha="center", va="center", fontsize=8, weight="bold")
for n, p in kp.items():
    ax.add_patch(Rectangle((p["x"] - p["w"] / 2, p["y"] - p["h"] / 2), p["w"], p["h"], fill=False, ec="#d9822b", lw=1.1, ls="--"))
for c in contact:
    y0 = c["y_centre"] - c["width"] / 2
    ax.add_patch(Rectangle((c["x"][0], y0), c["x"][1] - c["x"][0], c["width"], fc="#333", alpha=0.6))
ax.annotate("pin 1", (-3.0, -2.15), fontsize=9)
ax.set_xlim(-3.4, 3.4); ax.set_ylim(2.8, -2.8); ax.set_aspect("equal"); ax.grid(alpha=0.2)
ax.set_title("LTC2945CMS#PBF footprint check (KiCad frame, Y down, mm)", fontsize=10)
fig.text(0.02, 0.02, "teal: page pads = ADI MS12 recommended pad layout (0.42 x 0.889, pitch 0.65, gap 3.325);\n"
         "orange dashed: KiCad MSOP-12_3x4.039mm_P0.65mm pads; dark: 3D model lead contact at z = 0.01 mm; grey: body 3.00 x 4.039", fontsize=7.5, va="bottom")
fig.subplots_adjust(bottom=0.12); fig.savefig(op, bbox_inches="tight")
print(json.dumps({"all_pads_match": ok, "contact_inside": res["model_contact"]["all_inside"], "outer": outer}))