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
"""Render review images of the board into hardware/layout-review/ (needs kicad-cli, rsvg-convert, ImageMagick).

Colours: grey = board outline, rails and drawing notes; red = LED data-chain path (drawing only);
orange = copper pads; blue = front silkscreen (printed markings).
"""
import os
import subprocess
import sys
import tempfile

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import make_board as mb  # noqa: E402

OUT = os.path.join(mb.HW, "layout-review")
PX = 8                                    # pixels per mm (kept modest: larger images ran out of memory)
LAYERS = [("Edge.Cuts,Dwgs.User", "#808080"), ("Cmts.User", "#e02020"),
          ("F.Cu", "#c8742c"), ("F.SilkS", "#1f4fd1")]


def run(*cmd):
    subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)


ROUTED_SETS = {                    # name: [(layers, colour)] composited bottom-up
    "both": [("Edge.Cuts", "#808080"), ("B.Cu", "#3a78c9"), ("F.Cu", "#d0602a"), ("F.SilkS", "#1c1c1c")],
    "front": [("Edge.Cuts", "#808080"), ("F.Cu", "#d0602a"), ("F.SilkS", "#1c1c1c")],
    "back": [("Edge.Cuts", "#808080"), ("B.Cu", "#3a78c9")],
}


def render_set(tmp, layer_list, tag, pcb=None):
    parts = []
    for i, (layers, colour) in enumerate(layer_list):
        svg, png, col = (os.path.join(tmp, f"{tag}{i}.{e}") for e in ("svg", "png", "c.png"))
        run("kicad-cli", "pcb", "export", "svg", "-l", layers, "--black-and-white", "--exclude-drawing-sheet",
            "--page-size-mode", "0", "-o", svg, pcb or mb.PCB)
        run("rsvg-convert", "-w", str(420 * PX), svg, "-o", png)
        # front copper drawn semi-transparent over the back so both layers stay readable
        alpha = ["-channel", "A", "-evaluate", "multiply", "0.8", "+channel"] if layers == "F.Cu" and tag == "both" else []
        run("convert", png, "-fill", colour, "-colorize", "100", *alpha, col)
        parts.append(col)
    out = os.path.join(tmp, f"{tag}.png")
    run("convert", "-size", f"{420 * PX}x{297 * PX}", "xc:white", *parts, "-flatten", out)
    for p in parts:
        os.remove(p)
    return out


def crop_from(full, name, x0, y0, x1, y1, title=None, scale=None):
    px0, py0 = int((mb.X0 + x0) * PX), int((mb.Y0 + y0) * PX)
    w, h = int((x1 - x0) * PX), int((y1 - y0) * PX)
    cmd = ["convert", full, "-crop", f"{w}x{h}+{px0}+{py0}", "+repage"]
    if scale:
        cmd += ["-resize", scale]
    if title:
        cmd += ["-gravity", "north", "-background", "white", "-splice", "0x44", "-pointsize", "26",
                "-annotate", "+0+8", title]
    run(*cmd, os.path.join(OUT, name))


def routed():
    """Images of the routed board: orange = front copper, blue = back copper, black = silkscreen.
    Full-board and per-layer images include the copper fills; close-ups use a copy without fills so every
    track is visible."""
    import pcbnew
    tmp = tempfile.mkdtemp()
    both = render_set(tmp, ROUTED_SETS["both"], "both")
    crop_from(both, "routed-full-board.png", -2, -2, mb.W + 2, mb.H + 2, None, "2400x")
    os.remove(both)
    front = render_set(tmp, ROUTED_SETS["front"], "front")
    crop_from(front, "routed-front-copper.png", -2, -2, mb.W + 2, mb.H + 2, "Front copper: +5V fill, GND fill (USB/ESP32 strip), tracks", "2400x")
    os.remove(front)
    back = render_set(tmp, ROUTED_SETS["back"], "back")
    crop_from(back, "routed-back-copper.png", -2, -2, mb.W + 2, mb.H + 2, "Back copper: solid ground plane and the few short jumps", "2400x")
    os.remove(back)
    # copy without fills for the close-ups
    b = pcbnew.LoadBoard(mb.PCB)
    for z in [z for z in b.Zones() if not z.GetIsRuleArea()]:
        b.Remove(z)
    nofill = os.path.join(tmp, "nofill.kicad_pcb")
    pcbnew.SaveBoard(nofill, b)
    tr = render_set(tmp, ROUTED_SETS["both"], "tracks", nofill)
    crop_from(tr, "routed-full-tracks.png", -2, -2, mb.W + 2, mb.H + 2, "All tracks (fills hidden): orange front, blue back", "2400x")
    crop_from(tr, "routed-usb.png", 82, 50, 99, mb.H + 1, "USB-C fan-out, ESD chip U1, CC and VBUS jumps", "400%")
    crop_from(tr, "routed-power.png", 90, 51, 122, mb.H + 1, "Power input: VBUS, TVS D1, eFuse U2, 5 V out", "250%")
    crop_from(tr, "routed-esp32.png", 26, 47, 60, mb.H + 1, "ESP32, regulator, USB pair under the module (blue)", "250%")
    crop_from(tr, "routed-strip.png", 34, 49, 96, 64, "ESP32-to-USB-C strip: lanes, buttons, pull-ups", "160%")
    crop_from(tr, "routed-usb-pair.png", 26, 52, 94, mb.H + 1, "USB D+/D- route (front orange, back blue)", "160%")
    crop_from(tr, "routed-led-rail.png", 170, 0, 214, 56, "LED rail (letter E): data chain, capacitor links, vias", "200%")
    crop_from(tr, "routed-far-end.png", 256, 0, mb.W + 1, 71, "Far end: last letter, C304 bulk capacitor", "170%")
    crop_from(tr, "routed-led1-shifter.png", 0, 0, 22, 56, "LED data line and level shifter into LED1", "170%")
    os.remove(tr)
    print("routed images written to", OUT)


def main():
    if len(sys.argv) > 1 and sys.argv[1] == "routed":
        return routed()
    tmp = tempfile.mkdtemp()
    parts = []
    for i, (layers, colour) in enumerate(LAYERS):
        svg, png, col = (os.path.join(tmp, f"l{i}.{e}") for e in ("svg", "png", "c.png"))
        run("kicad-cli", "pcb", "export", "svg", "-l", layers, "--black-and-white", "--exclude-drawing-sheet",
            "--page-size-mode", "0", "-o", svg, mb.PCB)
        run("rsvg-convert", "-w", str(420 * PX), svg, "-o", png)
        run("convert", png, "-fill", colour, "-colorize", "100", col)
        parts.append(col)
    full = os.path.join(tmp, "full.png")
    run("convert", "-size", f"{420 * PX}x{297 * PX}", "xc:white", *parts, "-flatten", full)

    def crop(name, x0, y0, x1, y1, title=None, scale=None):
        px0, py0 = int((mb.X0 + x0) * PX), int((mb.Y0 + y0) * PX)
        w, h = int((x1 - x0) * PX), int((y1 - y0) * PX)
        cmd = ["convert", full, "-crop", f"{w}x{h}+{px0}+{py0}", "+repage"]
        if scale:
            cmd += ["-resize", scale]
        if title:
            cmd += ["-gravity", "north", "-background", "white", "-splice", "0x44", "-pointsize", "26",
                    "-annotate", "+0+8", title]
        run(*cmd, os.path.join(OUT, name))

    crop("placement-full-board.png", -3, -7, mb.W + 3, mb.H + 7, None, "2400x")
    ranges = ["LED1-18", "LED19-29", "LED30-40", "LED41-57", "LED58-75", "LED76-90", "LED91-107"]
    for i, ch in enumerate(mb.WORD):
        x0 = mb.COL0_X + i * 6 * mb.PITCH - 7
        crop(f"placement-letter-{i + 1}-{ch}.png", x0, 0, x0 + 42, 56,
             f"Letter {i + 1}: {ch}  ({ranges[i]})")
    crop("placement-esp32-antenna.png", 14, 48, 66, mb.H + 5.5,
         "ESP32 (U4) + antenna areas")
    crop("placement-usb-c.png", 78, 50, 102, mb.H + 5.5, "USB-C (J1) at the edge", "200%")
    crop("placement-led1-corner.png", -1, -1, 20, 14, "Hole + LED1: 0.61 mm gap", "250%")
    crop("placement-esp32-led-spacing.png", 30, 44, 50, 58, "LED18 to ESP32: 1.1 mm bodies", "250%")
    crop("placement-usb-c-pins.png", 84, 60, 96, mb.H + 1, "USB-C pins (0.20 mm local rule)", "400%")
    # component-placement close-ups
    crop("parts-usb-power.png", 82, 52, 123, mb.H + 1, "USB-C input, protection and eFuse", "200%")
    crop("parts-esp32-regulator.png", 24, 51, 62, mb.H + 1, "ESP32 support: USB, UART, regulator", "200%")
    crop("parts-buttons.png", 59, 52, 86, 65, "Buttons with labels", "250%")
    crop("parts-level-shifter.png", 0, 3, 16, 25, "Level shifter next to LED1", "300%")
    crop("parts-led-capacitors.png", 171, 2, 211, 55, "LED capacitors (letter E)", "200%")
    crop("parts-bulk-left.png", 5, 51, 24, mb.H + 1, "C301 bulk (fitted) + RX/TX/GND pads", "250%")
    crop("parts-bulk-right.png", 130, 53, 150, 66, "C302 bulk spot (DNP)", "250%")
    crop("parts-bulk-far-right.png", 274, 53, 294, 66, "C304 bulk (fitted)", "250%")
    print("images written to", OUT)


if __name__ == "__main__":
    main()