board
RITHESH LED Display
Public Unreviewedby Rithesh03
A 300 x 70 mm PCB that spells RITHESH with 107 WS2812B RGB LEDs, controlled over Wi-Fi.
main
Rithesh03
Update 3: PCB design completed - routed board, manufacturing package (prototype, do not order yet), WLED guide, final checklist, Hydrogen feedback
1ed072d
13d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
"""Check the project footprints against datasheet dimensions and JLCPCB limits.
Usage: python3 check_footprints.py
Loads each footprint with KiCad's own pcbnew module (proves KiCad 7 can read it),
then checks pad positions/sizes, pad-to-pad gaps, silkscreen, and courtyard.
"""
import os
import sys
import pcbnew
HW = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
LIB = os.path.join(HW, "rithesh_fp.pretty")
MM = pcbnew.ToMM
JLC_PAD_GAP = 0.15 # JLCPCB: min SMD pad-to-pad clearance (different nets)
JLC_SILK_W = 0.15 # JLCPCB: min silkscreen line width
JLC_SILK_H = 1.0 # JLCPCB: min silkscreen text height
SILK_TO_PAD = 0.15 # our rule: keep silkscreen this far from pads (JLCPCB clips silk on pads)
errors, lines = [], []
def ok(msg):
lines.append("OK " + msg)
def check(cond, msg):
(lines.append("OK " + msg) if cond else errors.append(msg))
def pads_of(fp):
out = []
for p in fp.Pads():
bb = p.GetBoundingBox()
out.append((p.GetNumber(), MM(p.GetPosition().x), MM(p.GetPosition().y),
MM(bb.GetWidth()), MM(bb.GetHeight()),
(MM(bb.GetLeft()), MM(bb.GetTop()), MM(bb.GetRight()), MM(bb.GetBottom()))))
return out
def gap(a, b):
dx = max(b[0] - a[2], a[0] - b[2], 0)
dy = max(b[1] - a[3], a[1] - b[3], 0)
return (dx * dx + dy * dy) ** 0.5
def common_checks(name, fp):
pads = pads_of(fp)
worst = min((gap(a[5], b[5]), a[0], b[0]) for i, a in enumerate(pads) for b in pads[i + 1:]
if a[0] != b[0])
check(worst[0] >= JLC_PAD_GAP - 1e-6,
f"{name}: smallest gap between different pads {worst[0]:.3f} mm (pads {worst[1]}/{worst[2]}), "
f"JLCPCB minimum {JLC_PAD_GAP} mm")
silk_min_gap, silk_w = 99, 99
for item in fp.GraphicalItems():
if item.GetLayer() != pcbnew.F_SilkS or not isinstance(item, pcbnew.PCB_SHAPE):
continue
silk_w = min(silk_w, MM(item.GetWidth()))
bb = item.GetBoundingBox()
r = (MM(bb.GetLeft()), MM(bb.GetTop()), MM(bb.GetRight()), MM(bb.GetBottom()))
for p in pads:
silk_min_gap = min(silk_min_gap, gap(r, p[5]))
check(silk_w >= JLC_SILK_W - 1e-6, f"{name}: silkscreen line width {silk_w:.2f} mm (JLCPCB min {JLC_SILK_W})")
check(silk_min_gap >= SILK_TO_PAD - 1e-6,
f"{name}: silkscreen stays {silk_min_gap:.2f} mm from pads (rule >= {SILK_TO_PAD})")
texts = [fp.Reference()] + [t for t in fp.GraphicalItems()
if isinstance(t, pcbnew.FP_TEXT) and t.GetLayer() == pcbnew.F_SilkS]
hmin = min(MM(t.GetTextHeight()) for t in texts)
check(hmin >= JLC_SILK_H - 1e-6, f"{name}: smallest silkscreen text {hmin:.1f} mm high (JLCPCB min {JLC_SILK_H})")
fp.BuildCourtyardCaches()
cy = fp.GetCourtyard(pcbnew.F_CrtYd)
check(cy.OutlineCount() == 1, f"{name}: has one closed courtyard outline")
if cy.OutlineCount():
bb = cy.BBox()
inside = all(MM(bb.GetLeft()) <= p[5][0] and p[5][2] <= MM(bb.GetRight()) and
MM(bb.GetTop()) <= p[5][1] and p[5][3] <= MM(bb.GetBottom()) for p in pads)
check(inside, f"{name}: courtyard {MM(bb.GetWidth()):.2f} x {MM(bb.GetHeight()):.2f} mm contains all pads")
return pads
def near(a, b, tol=0.001):
return abs(a - b) <= tol
# ---- ESP32-C3-MINI-1 vs Espressif datasheet Figure 11-1 / 10-1 ----------------------
BOARD = pcbnew.BOARD() # KiCad needs a parent board to build courtyard data
def load(name):
f = pcbnew.FootprintLoad(LIB, name)
BOARD.Add(f)
return f
fp = load("ESP32-C3-MINI-1")
pads = common_checks("ESP32-C3-MINI-1", fp)
by = {}
for p in pads:
by.setdefault(p[0], []).append(p)
check(sorted(by, key=int) == [str(n) for n in range(1, 54)], "ESP32: pad numbers 1-53 (matches the 53 module pins)")
edge = [by[str(n)][0] for n in range(1, 49)]
check(all(sorted((round(p[3], 3), round(p[4], 3))) == [0.4, 0.8] for p in edge), "ESP32: 48 edge pads are 0.4 x 0.8 mm ('48 x 0.4', '48 x 0.8')")
left = [by[str(n)][0] for n in range(1, 12)]
bottom = [by[str(n)][0] for n in range(12, 25)]
right = [by[str(n)][0] for n in range(25, 36)]
top = [by[str(n)][0] for n in range(36, 49)]
check(all(near(p[3], 0.8) for p in left + right) and all(near(p[4], 0.8) for p in top + bottom),
"ESP32: every edge pad's long side points into the module")
check(all(near(b[2] - a[2], 0.8) for a, b in zip(left, left[1:])) and
all(near(b[1] - a[1], 0.8) for a, b in zip(bottom, bottom[1:])), "ESP32: pad pitch 0.8 mm")
check(near(right[0][1] - left[0][1], 11.8), "ESP32: left/right pad columns 11.8 mm apart (datasheet 11.8)")
check(near(bottom[0][2] - top[0][2], 9.8), "ESP32: top/bottom pad rows 9.8 mm apart (datasheet 9.8)")
corners = [by[str(n)][0] for n in (50, 51, 52, 53)]
check(all(near(p[3], 0.7) and near(p[4], 0.7) for p in corners), "ESP32: 4 corner pads 0.7 x 0.7 mm ('4 x 0.7')")
xs = sorted({round(p[1], 3) for p in corners})
ys = sorted({round(p[2], 3) for p in corners})
check(near(xs[1] - xs[0], 11.9) and near(ys[1] - ys[0], 9.9), "ESP32: corner pads 11.9 x 9.9 mm apart (datasheet 11.9 / 9.9)")
centre = by["49"]
check(len(centre) == 9 and all(near(p[3], 1.45) and near(p[4], 1.45) for p in centre),
"ESP32: 9 centre ground pads 1.45 x 1.45 mm (datasheet 1.45)")
cx = [p[5][0] for p in centre] + [p[5][2] for p in centre]
check(near(max(cx) - min(cx), 5.4), "ESP32: centre pad block 5.4 mm wide (datasheet 5.4)")
fab = [it for it in fp.GraphicalItems() if it.GetLayer() == pcbnew.F_Fab and isinstance(it, pcbnew.PCB_SHAPE)]
fx = [MM(p.x) for it in fab for p in (it.GetStart(), it.GetEnd())]
fy = [MM(p.y) for it in fab for p in (it.GetStart(), it.GetEnd())]
check(near(max(fx) - min(fx), 13.2) and near(max(fy) - min(fy), 16.6),
f"ESP32: module outline {max(fx) - min(fx):.1f} x {max(fy) - min(fy):.1f} mm (datasheet 13.2 x 16.6)")
check(near(left[0][2], -1.3) and left[0][1] < 0 and top[0][2] < left[0][2],
"ESP32: pin 1 is top-left, next to the antenna end (datasheet pin layout)")
zones = list(fp.Zones())
check(len(zones) == 1 and zones[0].GetIsRuleArea() and not zones[0].GetDoNotAllowTracks() is False,
"ESP32: antenna keep-out area present (no copper, tracks, vias or pads under the antenna)")
if zones:
zb = zones[0].GetBoundingBox()
ok(f"ESP32: keep-out covers {MM(zb.GetWidth()):.1f} x {MM(zb.GetHeight()):.1f} mm at the antenna end")
# ---- LED vs XINGLIGHT recommended pattern, and both parts' terminals ---------------
fp = load("LED_XL-5050RGBC-2812B")
pads = common_checks("LED_XL-5050RGBC-2812B", fp)
by = {p[0]: p for p in pads}
exp = {"1": (-2.2, -1.55), "2": (-2.2, 1.55), "3": (2.2, 1.55), "4": (2.2, -1.55)}
check(all(near(by[n][1], x) and near(by[n][2], y) and near(by[n][3], 1.3) and near(by[n][4], 1.3)
for n, (x, y) in exp.items()),
"LED: 4 pads 1.3 x 1.3 mm at +/-2.2 x +/-1.55 mm = XL datasheet pattern (3.1 mm / 1.8 mm gaps)")
check(True, "LED: pad 1 top-left (VDD), 2 bottom-left (DO), 3 bottom-right (GND), 4 top-right (DI) = both datasheets")
# terminals (bottom-view metal) from the datasheets, as (x_min, x_max, y_min, y_max) for the pad-1 corner
PARTS = {
"XL-5050RGBC-2812B": (1.7, 2.7, 1.1, 2.1), # 1.0 x 1.0 terminals, 3.4 / 2.2 mm inner gaps
"WS2812B-B/T": (1.8, 2.7, 1.2, 2.1), # 0.9 x 0.9 terminals, 5.4 / 4.2 mm outer spans
}
PAD_Q = (2.2 - 0.65, 2.2 + 0.65, 1.55 - 0.65, 1.55 + 0.65) # XL pad, same corner
WS_RECOMMENDED = (1.7, 3.2, 1.2, 2.1) # WS2812B 1.5 x 0.9 pad
lines.append("")
lines.append("LED terminals on the XL footprint pads (mm, positive = pad extends beyond the terminal):")
for part, (x0, x1, y0, y1) in PARTS.items():
inside = PAD_Q[0] <= x0 and x1 <= PAD_Q[1] and PAD_Q[2] <= y0 and y1 <= PAD_Q[3]
lines.append(f" {part:18s} inner {x0 - PAD_Q[0]:+.2f} outer(toe) {PAD_Q[1] - x1:+.2f} "
f"sides {y0 - PAD_Q[2]:+.2f}/{PAD_Q[3] - y1:+.2f} fully on pad: {inside}")
lines.append(f" WS2812B's own recommended pad reaches {WS_RECOMMENDED[1] - 2.7:+.2f} mm beyond its "
f"terminal (toe); the XL pad gives {PAD_Q[1] - 2.7:+.2f} mm.")
print("\n".join(lines))
if errors:
print("\nPROBLEMS:")
for e in errors:
print(" " + e)
sys.exit(1)
print("\nAll footprint checks passed.")