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 4: improved README (V1 overview, images, LED order), Version 2 proposal files
b874429
13d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
"""Version 2 preliminary plan drawings (proposal only - no schematic or PCB yet).
Writes to v2/hardware/layout-plan/:
led-pattern-v2.svg / .png full board at 1:1 scale (print the SVG at 100 %): 107 LEDs, chain order, dimensions
board-plan-v2.svg / .png outline, mounting holes, keep-outs, LED area, bulk capacitors, electronics-area plan
electronics-area-v2.svg / .png close-up of the right-hand electronics area
footprint-LED_XL-2020RGBC-2812B.svg / .png the new LED footprint with dimensions and pin names
(The LED order file and WLED maps are written by make_v2_ledmaps.py into v2/firmware/.)
Coordinates: mm, origin at the board's top-left corner, y down (like KiCad).
The LED chain is Version 1's chain reversed: V2 LED n sits where V1 LED (108 - n) sits, so LED1 is next to
the electronics at the right end and the data line stays short. WLED maps for V2 = 106 - (V1 index).
Usage: python3 make_v2_plan.py
"""
import csv
import math
import os
import subprocess
V2HW = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ROOT = os.path.dirname(os.path.dirname(V2HW)) # project root (Version 1 lives here)
OUT = os.path.join(V2HW, "layout-plan")
V1_ORDER = os.path.join(ROOT, "firmware", "led-order.csv")
# ---- board -----------------------------------------------------------------------------
W, H, R_CORNER = 250.0, 40.0, 2.0
PITCH, LED = 5.0, 2.0 # LED spacing and body size
X0, Y0 = 9.1, 5.0 # centre of column 0 / row 0 (9.1: >= 1 mm from the hole keep-outs)
V1_PITCH, V1_X0 = 7.0, 10.0 # Version 1 grid, to convert led-order.csv
LED_CY = (2.70, 2.50) # LED courtyard (from the footprint)
HOLE_R_KEEP = 3.5 # 7 mm screw/washer keep-out (same as V1)
HOLES = [(3.2, 3.2), (3.2, H - 3.2), (215.0, 3.2), (215.0, H - 3.2)]
SCREW_HEAD_D = 5.7 # largest common M3 head (ISO 7380 button head; socket cap 5.5, pan head 5.6)
WASHER_D = 6.0 # 6 mm washer (the 7 mm keep-out also fits a DIN 125 7 mm washer)
LED_PAD_BOX = (2.2, 2.0) # outer edges of the four LED pads (footprint)
MIN_HOLE_LED = 0.5 # required: LED courtyard >= 0.5 mm outside the 7 mm keep-out (target ~1 mm)
EDGE_MIN = 0.3 # parts at least this far inside the board edge (except USB-C / antenna)
# ---- electronics area (courtyard rectangles: name, label, x, y, w, h, kind) ---------------
ESP = (233.4, 2.0, 16.6, 13.2) # module body, antenna end flush with the right edge
ANT_KEEP = (W - 5.4, 2.0, 5.4, 13.2) # module antenna keep-out, all copper layers (Espressif footprint)
# Espressif Hardware Design Guidelines Fig. 21: clearance >= 15 mm on each side of the antenna along the board edge,
# as deep as the antenna area (~6 mm). Below the antenna: a 15.5 mm x 6.0 mm no-copper zone (0.5 mm margin over 15).
# Above the antenna the board ends 2 mm later (free air); that 2 mm strip is also kept free of copper.
ANT_SIDE = (W - 6.0, 15.2, 6.0, 15.5)
ANT_TOP = (W - 6.0, 0.0, 6.0, 2.0)
ANT_REQ = 15.0 # required clearance along the edge
ZONE_MARGIN = 0.5 # every part stays at least this far outside the antenna zones
PARTS = [
("U4", "ESP32-C3-MINI-1", *ESP, "major"),
("J1", "USB-C", 238.0 - 5.32, H - 3.65 - 4.71, 10.64, 9.42, "edge"),
("D1", "SMAJ5.0A", 232.5, 27.45, 7.0, 3.5, "major"),
("U1", "USBLC6", 239.7, 27.2, 3.4, 3.4, "major"),
("U2", "eFuse", 227.9, 27.75, 3.2, 2.5, "major"),
("C301", "220uF", 223.5, 17.5, 7.1, 9.4, "major"),
("U3", "LDO", 225.2, 10.75, 2.66, 2.5, "major"),
("U5", "shifter", 211.9, 27.2, 3.4, 4.1, "major"),
("SW1", "RESET", 211.4, 8.25, 6.0, 3.5, "button"),
("SW2", "ON/OFF", 211.4, 14.75, 6.0, 3.5, "button"),
("SW3", "BOOT", 211.4, 21.25, 6.0, 3.5, "button"),
]
TPS = [("TP6", "LED_DIN", 216.6, 30.0)] + [
(f"TP{n}", name, 220.3 + 3.6 * (i % 4), 34.6 + 3.4 * (i // 4))
for i, (n, name) in enumerate([(1, "5V_IN"), (2, "5V_OUT"), (7, "FLT"), (8, "ILM"),
(3, "3V3"), (4, "GND"), (9, "RX"), (10, "TX")])
] + [("TP5", "GND", 246.8, 35.5)]
TP_CY = 2.5
ZONES = [ # dashed allowance areas for small passives
("ESP32 support: EN RC, pull-ups, 22R x2, CC 5.1k x2, 470R, 10uF x2", 222.4, 0.8, 10.6, 9.6),
("eFuse: 4.7uF, 100nF, 100k/39k, 100k, 1.5k || 1.5k", 222.4, 27.2, 5.2, 5.6),
("LED data: 33R, 100nF, 10k", 215.5, 25.2, 2.2, 3.4),
]
BULK_FAR = ("C302", "220uF (fitted, far end)", 36.0, 20.0)
BULK_MID = ("C303", "220uF spare (DNP)", 93.0, 22.0)
BULK_CY = (7.1, 9.4)
def leds():
"""[(n, letter, row, col, x, y)] for V2, LED1..LED107 (V1 chain reversed)."""
rows = list(csv.DictReader(open(V1_ORDER)))
out = []
for r in rows:
col = round((float(r["x mm"]) - V1_X0) / V1_PITCH)
row = int(r["row (0=top)"])
n = 108 - int(r["LED"][3:])
out.append((n, r["letter"], row, col, X0 + col * PITCH, Y0 + row * PITCH))
return sorted(out)
# ---- checks ------------------------------------------------------------------------------
def rect_overlap(a, b, gap=0.0):
ax, ay, aw, ah = a
bx, by, bw, bh = b
return ax < bx + bw + gap and bx < ax + aw + gap and ay < by + bh + gap and by < ay + ah + gap
def rect_circle_dist(r, c):
x, y, w, h = r
cx, cy = c
dx = max(x - cx, 0, cx - (x + w))
dy = max(y - cy, 0, cy - (y + h))
return math.hypot(dx, dy)
HOLE_REPORT, HOLE_EDGE, ANT_REPORT = {}, {}, {}
def checks(L):
problems, notes = [], []
led_r = [(x - LED_CY[0] / 2, y - LED_CY[1] / 2, *LED_CY) for *_, x, y in L]
rects = [(p[0], p[2:6], p[6]) for p in PARTS]
rects += [(t[0], (t[2] - TP_CY / 2, t[3] - TP_CY / 2, TP_CY, TP_CY), "tp") for t in TPS]
for ref, _, cx, cy in (BULK_FAR, BULK_MID):
rects.append((ref, (cx - BULK_CY[0] / 2, cy - BULK_CY[1] / 2, *BULK_CY), "bulk"))
# parts vs parts
for i in range(len(rects)):
for j in range(i + 1, len(rects)):
if rect_overlap(rects[i][1], rects[j][1]):
problems.append(f"{rects[i][0]} overlaps {rects[j][0]}")
# parts vs LEDs, holes, antenna, edge
for ref, r, kind in rects:
for k, lr in enumerate(led_r):
if rect_overlap(r, lr, 0.2):
problems.append(f"{ref} within 0.2 mm of LED{k + 1}")
for h in HOLES:
if rect_circle_dist(r, h) < HOLE_R_KEEP + 0.3:
problems.append(f"{ref} closer than 0.3 mm to the keep-out of hole at {h}")
if ref != "U4":
for name, a in (("antenna keep-out", ANT_KEEP), ("antenna clearance zone", ANT_SIDE),
("strip above the antenna", ANT_TOP)):
if rect_overlap(r, a, ZONE_MARGIN):
problems.append(f"{ref} closer than {ZONE_MARGIN} mm to the {name}")
x, y, w, h = r
if kind != "edge" and ref != "U4" and (x < EDGE_MIN or y < EDGE_MIN or x + w > W - EDGE_MIN or y + h > H - EDGE_MIN):
problems.append(f"{ref} closer than {EDGE_MIN} mm to the board edge")
# LEDs vs holes
for k, lr in enumerate(led_r):
for h in HOLES:
d = rect_circle_dist(lr, h)
if d < HOLE_R_KEEP:
problems.append(f"LED{k + 1} inside a hole keep-out")
# mounting-hole report: LED courtyard (both rotations), body and pads vs keep-out, screw head, washer
def boxes(x, y):
out = {}
for name, (w, h) in (("courtyard", LED_CY), ("body", (LED, LED)), ("pads", LED_PAD_BOX)):
out[name] = [(x - w / 2, y - h / 2, w, h), (x - h / 2, y - w / 2, h, w)] # 0 and 90 degrees
return out
worst = {}
for *_, x, y in L:
for name, bl in boxes(x, y).items():
for h in HOLES:
d = min(rect_circle_dist(b, h) for b in bl)
if name not in worst or d < worst[name][0]:
worst[name] = (d, h)
HOLE_REPORT.clear()
for name in ("courtyard", "body", "pads"):
d, h = worst[name]
HOLE_REPORT[name] = {"keep-out (7.0 mm)": d - HOLE_R_KEEP, f"washer ({WASHER_D} mm)": d - WASHER_D / 2,
f"screw head ({SCREW_HEAD_D} mm)": d - SCREW_HEAD_D / 2, "hole edge (3.2 mm)": d - 1.6}
if worst["courtyard"][0] - HOLE_R_KEEP < MIN_HOLE_LED:
problems.append(f"LED courtyard only {worst['courtyard'][0] - HOLE_R_KEEP:.2f} mm from a hole keep-out")
for (hx, hy) in HOLES:
edge = min(hx, hy, W - hx, H - hy)
HOLE_EDGE[(hx, hy)] = {"screw head inside board by": edge - SCREW_HEAD_D / 2, "washer inside board by": edge - WASHER_D / 2}
# antenna report: nearest part to the antenna keep-out, measured along the edge strip and in a straight line
ax0, ay0, aw, ah = ANT_KEEP
ANT_REPORT.clear()
ANT_REPORT["clearance zone below the antenna"] = ANT_SIDE[3]
strip = [r for ref, r, k in rects if ref != "U4" and r[0] + r[2] > ANT_SIDE[0]]
ANT_REPORT["nearest part in the edge strip below the antenna"] = min(r[1] - (ay0 + ah) for r in strip if r[1] > ay0)
ANT_REPORT["nearest part to the zone boundary"] = min(
min(max(z[0] - (r[0] + r[2]), r[0] - (z[0] + z[2]), z[1] - (r[1] + r[3]), r[1] - (z[1] + z[3]))
for z in (ANT_KEEP, ANT_SIDE, ANT_TOP)) for ref, r, k in rects if ref != "U4")
if ANT_SIDE[3] < ANT_REQ:
problems.append("antenna clearance zone shorter than 15 mm")
# rounded corner at the ESP32
ex, ey, ew, eh = ESP
corner = (W - R_CORNER, R_CORNER)
d = math.hypot(ex + ew - corner[0], ey - corner[1])
if ex + ew > W - R_CORNER and ey < R_CORNER and d > R_CORNER + 1e-6:
problems.append("ESP32 corner outside the rounded board corner")
# free area estimate in the electronics section
area = (W - 211.0) * H
used = sum(r[2] * r[3] for ref, r, k in rects if r[0] > 205) + sum(z[3] * z[4] for z in ZONES)
used += (ANT_KEEP[2] * ANT_KEEP[3] + ANT_SIDE[2] * ANT_SIDE[3] + ANT_TOP[2] * ANT_TOP[3]
+ 2 * math.pi * HOLE_R_KEEP ** 2 / 2)
notes.append(f"electronics area x 211.0-{W:.0f} mm: {area:.0f} mm2, parts + keep-outs + passive zones "
f"{used:.0f} mm2 ({100 * used / area:.0f} %), free for routing {area - used:.0f} mm2")
return problems, notes
# ---- SVG helpers ---------------------------------------------------------------------------
C = {"board": "#1f5f3a", "edge": "#e8e2c8", "led": "#f4f4f4", "ledline": "#111", "chain": "#ffb000",
"hole": "#b33", "keep": "#ff6b6b", "ant": "#4aa3ff", "part": "#d9d9d9", "text": "#fff", "dim": "#ffd84d",
"bulk": "#c89bff", "tp": "#ffcc66", "zone": "#9fe39f", "btn": "#8fd0ff"}
def svg_open(vx, vy, vw, vh, scale_mm=True):
unit = "mm" if scale_mm else ""
return (f'<svg xmlns="http://www.w3.org/2000/svg" width="{vw}{unit}" height="{vh}{unit}" '
f'viewBox="{vx} {vy} {vw} {vh}" font-family="DejaVu Sans, Arial, sans-serif">\n'
f'<rect x="{vx}" y="{vy}" width="{vw}" height="{vh}" fill="#0d1117"/>\n')
def board_outline():
return (f'<rect x="0" y="0" width="{W}" height="{H}" rx="{R_CORNER}" fill="{C["board"]}" '
f'stroke="{C["edge"]}" stroke-width="0.25"/>\n')
def text(x, y, s, size=1.6, color=None, anchor="middle", weight="normal", rot=0):
t = f' transform="rotate({rot} {x} {y})"' if rot else ""
s = s.replace("&", "&").replace("<", "<")
return (f'<text x="{x:.2f}" y="{y:.2f}" font-size="{size}" fill="{color or C["text"]}" '
f'text-anchor="{anchor}" font-weight="{weight}"{t}>{s}</text>\n')
def hdim(x1, x2, y, label, size=1.5):
s = f'<g stroke="{C["dim"]}" stroke-width="0.15">'
s += f'<line x1="{x1}" y1="{y}" x2="{x2}" y2="{y}"/><line x1="{x1}" y1="{y - 1}" x2="{x1}" y2="{y + 1}"/>'
s += f'<line x1="{x2}" y1="{y - 1}" x2="{x2}" y2="{y + 1}"/></g>\n'
return s + text((x1 + x2) / 2, y - 0.6, label, size, C["dim"])
def vdim(y1, y2, x, label, size=1.5):
s = f'<g stroke="{C["dim"]}" stroke-width="0.15">'
s += f'<line x1="{x}" y1="{y1}" x2="{x}" y2="{y2}"/><line x1="{x - 1}" y1="{y1}" x2="{x + 1}" y2="{y1}"/>'
s += f'<line x1="{x - 1}" y1="{y2}" x2="{x + 1}" y2="{y2}"/></g>\n'
return s + text(x - 0.8, (y1 + y2) / 2, label, size, C["dim"], rot=-90)
def holes_svg():
s = ""
for (x, y) in HOLES:
s += (f'<circle cx="{x}" cy="{y}" r="{HOLE_R_KEEP}" fill="none" stroke="{C["keep"]}" '
f'stroke-width="0.15" stroke-dasharray="0.6 0.4"/>')
s += f'<circle cx="{x}" cy="{y}" r="1.6" fill="#0d1117" stroke="{C["hole"]}" stroke-width="0.2"/>\n'
return s
def leds_svg(L, numbers=True, chain=True):
s = ""
if chain:
pts = " ".join(f"{x:.2f},{y:.2f}" for *_, x, y in L)
s += (f'<polyline points="{pts}" fill="none" stroke="{C["chain"]}" stroke-width="0.25" '
f'stroke-opacity="0.8"/>\n')
for n, letter, row, col, x, y in L:
s += (f'<rect x="{x - LED / 2}" y="{y - LED / 2}" width="{LED}" height="{LED}" rx="0.2" '
f'fill="{C["led"]}" stroke="{C["ledline"]}" stroke-width="0.08"/>')
if numbers:
s += text(x, y + 0.45, str(n), 1.05, "#111", weight="bold")
s += "\n"
return s
def rect_svg(x, y, w, h, fill, label="", size=1.2, dash=False, opacity=0.9, stroke="#000"):
d = ' stroke-dasharray="0.8 0.5"' if dash else ""
s = (f'<rect x="{x:.2f}" y="{y:.2f}" width="{w:.2f}" height="{h:.2f}" fill="{fill}" fill-opacity="{opacity}" '
f'stroke="{stroke}" stroke-width="0.12"{d}/>\n')
if label:
lines = label.split("\n")
for i, ln in enumerate(lines):
s += text(x + w / 2, y + h / 2 + (i - (len(lines) - 1) / 2) * size * 1.15 + size * 0.35, ln, size, "#111")
return s
def electronics_svg(detail=True):
s = ""
for z in ZONES:
s += rect_svg(*z[1:], C["zone"], "", dash=True, opacity=0.35)
for ref, label, x, y, w, h, kind in PARTS:
fill = C["btn"] if kind == "button" else C["part"]
if ref == "U4":
s += rect_svg(x, y, w - 5.4, h, fill, "ESP32-C3\nMINI-1 (U4)" if detail else "U4", 1.0)
s += rect_svg(x + w - 5.4, y, 5.4, h, fill, "")
elif ref == "J1":
s += rect_svg(x, y, w, h, fill, "USB-C (J1)", 1.0)
elif ref == "C301":
s += f'<circle cx="{x + w / 2}" cy="{y + h / 2}" r="3.15" fill="{C["bulk"]}" stroke="#000" stroke-width="0.12"/>'
s += text(x + w / 2, y + h / 2 + 0.4, "C301", 1.0, "#111")
s += text(x + w / 2, y + h / 2 + 1.5, "220uF", 0.8, "#111")
elif kind == "button":
s += rect_svg(x, y, w, h, fill, ref, 0.9)
s += text(x + w + 0.5, y + h / 2 + 0.45, label, 1.2, C["text"], anchor="start", weight="bold")
else:
s += rect_svg(x, y, w, h, fill, f"{ref}\n{label}" if detail else ref, 0.6)
for ref, name, x, y in TPS:
ly = y - 1.05 if 34 < y < 35 else y + 1.5 # label above the top test-pad row
s += (f'<circle cx="{x}" cy="{y}" r="0.75" fill="{C["tp"]}" stroke="#000" stroke-width="0.1"/>'
+ text(x, ly, name, 0.6, C["text"]) + "\n")
# antenna areas drawn last so they stay visible over the module
s += rect_svg(*ANT_KEEP, C["ant"], "antenna\nkeep-out" if detail else "", 0.8, opacity=0.55)
s += rect_svg(*ANT_SIDE, C["ant"], "antenna\nclearance\n15.5 mm\nno copper\nboth layers" if detail else "",
0.75, dash=True, opacity=0.3)
s += '<g clip-path="url(#board)">' + rect_svg(*ANT_TOP, C["ant"], "", dash=True, opacity=0.3) + "</g>\n"
if detail:
# 5 V feed and data arrows
s += (f'<path d="M 223.5 25.5 L 210.5 25.5" stroke="#ff4040" stroke-width="0.6" fill="none" '
f'marker-end="url(#arr)"/>' + text(216.7, 26.9, "5 V feed to LEDs", 0.7, "#ff8080"))
s += (f'<path d="M 211.9 31.3 L 210.2 34.2" stroke="{C["chain"]}" stroke-width="0.35" fill="none" '
f'marker-end="url(#arr2)"/>')
return s
DEFS = (f'<defs><clipPath id="board"><rect x="0" y="0" width="{W}" height="{H}" rx="{R_CORNER}"/></clipPath>'
'<marker id="arr" markerWidth="4" markerHeight="4" refX="3" refY="2" orient="auto">'
'<path d="M0,0 L4,2 L0,4 z" fill="#ff4040"/></marker>'
'<marker id="arr2" markerWidth="4" markerHeight="4" refX="3" refY="2" orient="auto">'
'<path d="M0,0 L4,2 L0,4 z" fill="#ffb000"/></marker></defs>\n')
def write(name, svg, dpi):
p = os.path.join(OUT, name + ".svg")
open(p, "w").write(svg + "</svg>\n")
subprocess.run(["rsvg-convert", "-d", str(dpi), "-p", str(dpi), "-o", os.path.join(OUT, name + ".png"), p],
check=True)
def pattern_svg(L):
m = 14 # margin around the board for dimensions
s = svg_open(-m, -m + 2, W + 2 * m, H + 2 * m + 4) + DEFS
s += board_outline() + holes_svg() + leds_svg(L)
s += electronics_svg(detail=False)
# letter boxes and names
for i, letter in enumerate("RITHESH"):
x1 = X0 + i * 6 * PITCH - LED / 2
s += text(x1 + 11, -1.2, letter, 2.2, C["dim"], weight="bold")
s += hdim(0, W, H + 4, f"{W:.0f} mm")
s += vdim(0, H, -5, f"{H:.0f} mm")
s += hdim(X0 - LED / 2, X0 + 40 * PITCH + LED / 2, -6.5, "word: 202 mm (41 columns x 5 mm)")
s += hdim(X0 - LED / 2, X0 + 4 * PITCH + LED / 2, H + 8.5, "letter 22 mm")
s += hdim(X0 + 4 * PITCH + LED / 2, X0 + 6 * PITCH - LED / 2, H + 8.5, "gap 8", 1.2)
s += vdim(Y0 - LED / 2, Y0 + 6 * PITCH + LED / 2, W + 7, "letter 32 mm")
s += text(W / 2, H + 13.5, "RITHESH V2 (proposal) - 107 x XL-2020RGBC-2812B, 5 mm pitch - numbers = chain order "
"(LED1 at the right end, next to the level shifter) - scale 1:1 when printed at 100 %", 1.4, "#ccc")
return s
def plan_svg(L):
m = 6
s = svg_open(-m, -m, W + 2 * m, H + 2 * m + 6) + DEFS
s += board_outline() + holes_svg()
s += rect_svg(X0 - 1.35, Y0 - 1.25, 40 * PITCH + 2.7, 6 * PITCH + 2.5, "#2d7a4f", "", opacity=0.6, dash=True,
stroke="#9fe39f")
s += leds_svg(L, numbers=False, chain=False)
for ref, label, cx, cy in (BULK_FAR, BULK_MID):
s += (f'<circle cx="{cx}" cy="{cy}" r="3.15" fill="{C["bulk"]}" stroke="#000" stroke-width="0.12"/>'
+ text(cx, cy + 0.4, ref, 1.1, "#111") + text(cx, cy + 5.2, label, 1.0, C["text"]))
s += electronics_svg(detail=True)
s += text(W / 2, H + 4.2, "V2 board plan (proposal): 250 x 40 mm, 2 layers - LED area (dashed green), 4 x M3 "
"holes with 7 mm keep-outs (red), electronics at the right end, antenna at the top-right corner", 1.4, "#ccc")
return s
def electronics_close_svg(L):
vx, vw = 203.0, 50.0
s = svg_open(vx, -3, vw, H + 9) + DEFS
s += board_outline() + holes_svg() + leds_svg([l for l in L if l[4] > 190], numbers=True, chain=True)
s += electronics_svg(detail=True)
s += hdim(211.0, W, H + 3.2, f"electronics area {W - 211.0:.1f} mm", 1.2)
s += text(vx + vw / 2, H + 5.4, "Plan only: part outlines are courtyards; small passives fit in the dashed "
"green zones. Final positions come at placement.", 0.95, "#ccc")
return s
def footprint_svg():
import importlib.util
spec = importlib.util.spec_from_file_location("fp", os.path.join(V2HW, "tools", "make_v2_footprint.py"))
fp = importlib.util.module_from_spec(spec)
spec.loader.exec_module(fp)
k = 10 # drawing scale (10 x)
s = ('<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="760" viewBox="-25 -19 50 38" '
'font-family="DejaVu Sans, Arial, sans-serif">\n<rect x="-25" y="-19" width="50" height="38" fill="#0d1117"/>\n')
b = fp.BODY * k
s += (f'<rect x="{-b}" y="{-b}" width="{2 * b}" height="{2 * b}" fill="none" stroke="#888" stroke-width="0.12" '
f'stroke-dasharray="0.6 0.4"/>')
s += (f'<rect x="{-fp.CY_X * k}" y="{-fp.CY_Y * k}" width="{2 * fp.CY_X * k}" height="{2 * fp.CY_Y * k}" '
f'fill="none" stroke="#ff66cc" stroke-width="0.1"/>')
for num, name, x, y in fp.PADS:
s += (f'<rect x="{(x - fp.PAD_W / 2) * k}" y="{(y - fp.PAD_H / 2) * k}" width="{fp.PAD_W * k}" '
f'height="{fp.PAD_H * k}" fill="#c8323c"/>')
s += text(x * k, y * k + 0.9, num, 2.6, "#fff", weight="bold")
s += text(x * k * 2.05, y * k + 0.6, name, 1.9, "#ffd84d", weight="bold")
s += f'<circle cx="{0.5 * k}" cy="{0.7 * k}" r="{0.14 * k}" fill="none" stroke="#aaa" stroke-width="0.15"/>'
s += f'<circle cx="{1.6 * k}" cy="{1.25 * k}" r="{0.15 * k}" fill="#fff"/>'
for yy in (-fp.SILK_Y, fp.SILK_Y):
s += (f'<line x1="{-fp.SILK_X * k}" y1="{yy * k}" x2="{fp.SILK_X * k}" y2="{yy * k}" stroke="#fff" '
f'stroke-width="{fp.SILK_W * k}" stroke-opacity="0.8"/>')
px = (fp.PX + fp.PAD_W / 2) * k
py = (fp.PY + fp.PAD_H / 2) * k
s += hdim(-px, px, -py - 5.0, "2.20 (pattern)", 1.3)
s += hdim(-b, b, py + 5.5, "2.00 body", 1.3)
s += hdim((fp.PX - fp.PAD_W / 2) * k, px, -py - 3, "0.85", 1.2)
s += hdim(-(fp.PX - fp.PAD_W / 2) * k, (fp.PX - fp.PAD_W / 2) * k, -py - 3, "0.5", 1.2)
s += vdim((fp.PY - fp.PAD_H / 2) * k, py, px + 9.5, "0.75", 1.2)
s += vdim(-(fp.PY - fp.PAD_H / 2) * k, (fp.PY - fp.PAD_H / 2) * k, px + 9.5, "0.5", 1.2)
s += vdim(-py, py, -px - 7.5, "2.00 (pattern)", 1.3)
s += text(0, -17.2, "LED_XL-2020RGBC-2812B (top view, 10x) - pin 1 = DO at the marked corner", 1.3, "#ccc")
s += text(0, 17.0, "pads 0.85 x 0.75 mm, centres +/-0.675 x +/-0.625 mm, gaps 0.5 mm", 1.05, "#ccc")
s += text(0, 18.5, "grey dashed = 2.0 mm body, grey circle = top mark, white = silkscreen, pink = courtyard",
0.95, "#aaa")
return s
def main():
os.makedirs(OUT, exist_ok=True)
L = leds()
assert len(L) == 107 and [l[0] for l in L] == list(range(1, 108))
problems, notes = checks(L)
write("led-pattern-v2", pattern_svg(L), 254) # 10 px per mm
write("board-plan-v2", plan_svg(L), 254)
write("electronics-area-v2", electronics_close_svg(L), 762) # 30 px per mm
svg = footprint_svg()
p = os.path.join(OUT, "footprint-LED_XL-2020RGBC-2812B.svg")
open(p, "w").write(svg + "</svg>\n")
subprocess.run(["rsvg-convert", "-w", "1500", "-o", p[:-4] + ".png", p], check=True)
for n in notes:
print("note:", n)
print("mounting holes: clearance of the closest LED (worst of 0 / 90 degree rotation), mm")
for name, d in HOLE_REPORT.items():
print(f" LED {name:9}: " + ", ".join(f"{k} {v:.2f}" for k, v in d.items()))
for h, d in HOLE_EDGE.items():
print(f" hole {h}: " + ", ".join(f"{k} {v:.2f}" for k, v in d.items()))
print("antenna (mm):", ", ".join(f"{k} {v:.2f}" for k, v in ANT_REPORT.items()))
print("CHECKS:", "all passed" if not problems else f"{len(problems)} problem(s)")
for p in problems:
print(" -", p)
print("LED1 at", L[0][4:], "LED107 at", L[-1][4:])
if __name__ == "__main__":
main()