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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728
"""Generate the RITHESH LED Display KiCad 7 schematic.

Run from anywhere:  python3 hardware/tools/generate_schematic.py
Writes the .kicad_pro, .kicad_sch sheets, the project symbol library and
sym-lib-table into the hardware/ folder. Every connection is made with a short
wire from the pin to a net label or power symbol, so each pin's net is readable.
"""
import copy
import os
import uuid

import sexp
from sexp import Sym as S

HERE = os.path.dirname(os.path.abspath(__file__))
OUT = os.path.dirname(HERE)
PROJECT = "rithesh-led-display"
KICAD_SYMBOLS = "/usr/share/kicad/symbols/"
DATE = "2026-09-23"
REV = "0.1 (prototype)"

NS = uuid.UUID("5f0e6a4e-6d2b-4d7e-9a57-7a1c3c2d9b10")


GRID = 1.27


def g(v):
    """Snap a coordinate to KiCad's 1.27 mm (50 mil) grid."""
    return round(round(v / GRID) * GRID, 4)


def U(key):
    """Deterministic UUID so re-running the generator gives stable files."""
    return str(uuid.uuid5(NS, key))


ROOT_UUID = U("root")

# ---------------------------------------------------------------- libraries
_lib_cache = {}


def _lib(lib):
    if lib not in _lib_cache:
        tree = sexp.parse(open(KICAD_SYMBOLS + lib + ".kicad_sym").read())
        _lib_cache[lib] = {s[1]: s for s in sexp.find(tree, "symbol")}
    return _lib_cache[lib]


def _rename_units(sym, old, new):
    for x in sym:
        if isinstance(x, list) and x and x[0] == "symbol" and x[1].startswith(old + "_"):
            x[1] = new + x[1][len(old):]


def kicad_symbol(lib, name):
    """Copy a KiCad library symbol, flattening 'extends' (derived) symbols."""
    src = _lib(lib)[name]
    ext = sexp.find1(src, "extends")
    if ext:
        base = copy.deepcopy(_lib(lib)[ext[1]])
        _rename_units(base, ext[1], name)
        base[:] = [x for x in base if not (isinstance(x, list) and x and x[0] == "property")]
        props = [copy.deepcopy(p) for p in sexp.find(src, "property")]
        idx = next(i for i, x in enumerate(base) if isinstance(x, list) and x and x[0] == "symbol")
        base[idx:idx] = props
        sym = base
    else:
        sym = copy.deepcopy(src)
    sym[1] = f"{lib}:{name}"
    return sym


def _font(size=1.27):
    return [S("effects"), [S("font"), [S("size"), size, size]]]


def _prop(key, val, x, y, hide=False, size=1.27, justify=None):
    eff = _font(size)
    if justify:
        eff.append([S("justify")] + [S(j) for j in justify.split()])
    if hide:
        eff.append(S("hide"))
    return [S("property"), key, val, [S("at"), x, y, 0], eff]


def custom_symbol(name, ref, value, body, pins, footprint, datasheet, desc, lcsc=""):
    """Build a simple rectangular symbol. pins: (num, name, type, x, y, angle, hidden)."""
    x1, y1, x2, y2 = body
    sym = [S("symbol"), name,
           [S("pin_names"), [S("offset"), 1.016]],
           [S("in_bom"), S("yes")], [S("on_board"), S("yes")],
           _prop("Reference", ref, x1, y1 + 1.27, justify="left bottom"),
           _prop("Value", value, x1, y2 - 1.27, justify="left top"),
           _prop("Footprint", footprint, 0, 0, hide=True),
           _prop("Datasheet", datasheet, 0, 0, hide=True),
           _prop("Description", desc, 0, 0, hide=True)]
    if lcsc:
        sym.append(_prop("LCSC", lcsc, 0, 0, hide=True))
    sym.append([S("symbol"), name + "_0_1",
                [S("rectangle"), [S("start"), x1, y1], [S("end"), x2, y2],
                 [S("stroke"), [S("width"), 0.254], [S("type"), S("default")]],
                 [S("fill"), [S("type"), S("background")]]]])
    unit = [S("symbol"), name + "_1_1"]
    for num, pname, ptype, px, py, ang, hidden in pins:
        p = [S("pin"), S(ptype), S("line"), [S("at"), px, py, ang], [S("length"), 2.54]]
        if hidden:
            p.append(S("hide"))
        p += [[S("name"), pname, _font()], [S("number"), num, _font()]]
        unit.append(p)
    sym.append(unit)
    return sym


def build_custom_symbols():
    syms = {}
    # TPS259531 eFuse, TI DSG (WSON-8) package. Pin numbers from TI datasheet SLVSE57C.
    syms["TPS259531"] = custom_symbol(
        "TPS259531", "U", "TPS259531DSGR", (-10.16, 7.62, 10.16, -7.62),
        [("3", "IN", "power_in", -12.7, 5.08, 0, False),
         ("4", "IN", "passive", -12.7, 5.08, 0, True),
         ("2", "EN/UVLO", "input", -12.7, 0, 0, False),
         ("1", "dVdt", "passive", -12.7, -5.08, 0, False),
         ("5", "OUT", "power_out", 12.7, 5.08, 180, False),
         ("6", "~{FLT}", "open_collector", 12.7, 0, 180, False),
         ("7", "ILM", "passive", 12.7, -5.08, 180, False),
         ("8", "GND", "power_in", 0, -10.16, 90, False),
         ("9", "PAD", "passive", 0, -10.16, 90, True)],
        "Package_SON:WSON-8-1EP_2x2mm_P0.5mm_EP0.9x1.6mm",
        "https://www.ti.com/lit/ds/symlink/tps2595.pdf",
        "2.7-18V 4A eFuse, 5.7V overvoltage clamp, auto-retry", "C2155674")
    # TLV76733 fixed 3.3 V LDO, TI DRV (WSON-6) package, fixed-output pinout.
    syms["TLV76733DRV"] = custom_symbol(
        "TLV76733DRV", "U", "TLV76733DRVR", (-7.62, 5.08, 7.62, -5.08),
        [("6", "IN", "power_in", -10.16, 2.54, 0, False),
         ("4", "EN", "input", -10.16, -2.54, 0, False),
         ("1", "OUT", "power_out", 10.16, 2.54, 180, False),
         ("2", "SNS", "input", 10.16, -2.54, 180, False),
         ("3", "GND", "power_in", 0, -7.62, 90, False),
         ("5", "GND", "passive", 0, -7.62, 90, True),
         ("7", "PAD", "passive", 0, -7.62, 90, True)],
        "Package_SON:WSON-6-1EP_2x2mm_P0.65mm_EP1x1.6mm",
        "https://www.ti.com/lit/ds/symlink/tlv767.pdf",
        "1A low-dropout regulator, fixed 3.3V, 16V input", "C2848334")
    # ESP32-C3-MINI-1 module. Pin numbers from Espressif datasheet Table 3-1.
    esp = [("3", "3V3", "power_in", -17.78, 20.32, 0, False),
           ("8", "EN", "input", -17.78, 15.24, 0, False),
           ("12", "IO0", "bidirectional", -17.78, 10.16, 0, False),
           ("13", "IO1", "bidirectional", -17.78, 7.62, 0, False),
           ("5", "IO2", "bidirectional", -17.78, 2.54, 0, False),
           ("6", "IO3", "bidirectional", -17.78, 0, 0, False),
           ("18", "IO4", "bidirectional", -17.78, -2.54, 0, False),
           ("19", "IO5", "bidirectional", -17.78, -7.62, 0, False),
           ("20", "IO6", "bidirectional", 17.78, 20.32, 180, False),
           ("21", "IO7", "bidirectional", 17.78, 17.78, 180, False),
           ("22", "IO8", "bidirectional", 17.78, 12.7, 180, False),
           ("23", "IO9", "bidirectional", 17.78, 10.16, 180, False),
           ("16", "IO10", "bidirectional", 17.78, 5.08, 180, False),
           ("26", "IO18/USB_D-", "bidirectional", 17.78, 0, 180, False),
           ("27", "IO19/USB_D+", "bidirectional", 17.78, -2.54, 180, False),
           ("30", "RXD0/IO20", "bidirectional", 17.78, -7.62, 180, False),
           ("31", "TXD0/IO21", "bidirectional", 17.78, -10.16, 180, False),
           ("1", "GND", "power_in", 0, -25.4, 90, False)]
    for n in [2, 11, 14] + list(range(36, 54)):
        esp.append((str(n), "GND", "passive", 0, -25.4, 90, True))
    # Module NC pads: hidden, each at its own spot so KiCad does not see them as joined.
    for i, n in enumerate([4, 7, 9, 10, 15, 17, 24, 25, 28, 29, 32, 33, 34, 35]):
        esp.append((str(n), "NC", "no_connect", -8.89 + i * 1.27, 25.4, 270, True))
    syms["ESP32-C3-MINI-1"] = custom_symbol(
        "ESP32-C3-MINI-1", "U", "ESP32-C3-MINI-1-N4", (-15.24, 22.86, 15.24, -22.86), esp,
        "rithesh_fp:ESP32-C3-MINI-1",
        "https://documentation.espressif.com/esp32-c3-mini-1_datasheet_en.pdf",
        "ESP32-C3 Wi-Fi module, 4MB flash, PCB antenna (official Espressif footprint)",
        "C2838502")
    # XL-5050RGBC-2812B: same graphics and pin numbers as KiCad's WS2812B symbol.
    xl = kicad_symbol("LED", "WS2812B")
    _rename_units(xl, "WS2812B", "XL-5050RGBC-2812B")
    xl[1] = "XL-5050RGBC-2812B"
    for p in sexp.find(xl, "property"):
        if p[1] == "Value":
            p[2] = "XL-5050RGBC-2812B"
        elif p[1] == "Footprint":
            p[2] = "rithesh_fp:LED_XL-5050RGBC-2812B"
        elif p[1] == "Datasheet":
            p[2] = "https://jlcpcb.com/partdetail/C2843785"
        elif p[1] == "Description":
            p[2] = "Addressable RGB LED, 5050, 3.5-5.5V, 12mA/color (PROVISIONAL)"
        elif p[1] == "Reference":
            p[2] = "LED"
    syms["XL-5050RGBC-2812B"] = xl
    return syms


CUSTOM = build_custom_symbols()


def lib_symbol(lib_id):
    lib, name = lib_id.split(":")
    if lib == "rithesh":
        s = copy.deepcopy(CUSTOM[name])
        _rename_units(s, name, name)
        s[1] = lib_id
        return s
    return kicad_symbol(lib, name)


def pin_table(symdef):
    """(number, x, y, angle, type, hidden) for unit 0/1, style 1 pins."""
    out = []
    for unit in sexp.find(symdef, "symbol"):
        parts = unit[1].rsplit("_", 2)
        if parts[1] not in ("0", "1") or parts[2] not in ("0", "1"):
            continue
        for p in sexp.find(unit, "pin"):
            at = sexp.find1(p, "at")
            out.append((sexp.find1(p, "number")[1], float(at[1]), float(at[2]),
                        int(float(at[3])), str(p[1]), S("hide") in p))
    return out


# ---------------------------------------------------------------- sheets
_pwr_count = [0]
OUTWARD = {0: (-1, 0), 180: (1, 0), 90: (0, 1), 270: (0, -1)}
POWER_ROT_UP = {(0, -1): 0, (0, 1): 180, (-1, 0): 90, (1, 0): 270}    # +5V style
POWER_ROT_DOWN = {(0, 1): 0, (0, -1): 180, (1, 0): 90, (-1, 0): 270}  # GND style
POWER_LIB = {"GND": "power:GND", "+5V": "power:+5V", "+3V3": "power:+3V3", "VBUS": "power:VBUS"}


class Sheet:
    def __init__(self, fname, title, paper, page, comments=()):
        self.fname, self.title, self.paper, self.page = fname, title, paper, page
        self.comments = comments
        self.items, self.libs = [], {}
        self.uuid = U("file:" + fname)
        self.sheet_uuid = U("sheet:" + fname)
        self.path = "/" if fname.startswith(PROJECT) else f"/{ROOT_UUID}/{self.sheet_uuid}"
        self.inst_path = f"/{ROOT_UUID}" if self.path == "/" else self.path
        self.nets = {}

    # -- primitives
    def wire(self, x1, y1, x2, y2):
        self.items.append([S("wire"), [S("pts"), [S("xy"), x1, y1], [S("xy"), x2, y2]],
                           [S("stroke"), [S("width"), 0], [S("type"), S("default")]],
                           [S("uuid"), U(f"{self.fname}:w:{x1}:{y1}:{x2}:{y2}")]])

    def text(self, s, x, y, size=1.8, bold=False):
        eff = [S("effects"), [S("font"), [S("size"), size, size]] + ([S("bold")] if bold else []),
               [S("justify"), S("left"), S("top")]]
        self.items.append([S("text"), s, [S("at"), x, y, 0], eff,
                           [S("uuid"), U(f"{self.fname}:t:{x}:{y}:{s[:20]}")]])

    def box(self, x1, y1, x2, y2, title=None):
        self.items.append([S("rectangle"), [S("start"), x1, y1], [S("end"), x2, y2],
                           [S("stroke"), [S("width"), 0.3], [S("type"), S("dash")]],
                           [S("fill"), [S("type"), S("none")]],
                           [S("uuid"), U(f"{self.fname}:r:{x1}:{y1}")]])
        if title:
            self.text(title, x1 + 2, y1 + 2, size=2.5, bold=True)

    def label(self, name, x, y, o, glob=False, shape="bidirectional"):
        ang = {(1, 0): 0, (-1, 0): 180, (0, -1): 90, (0, 1): 270}[o]
        just = "left" if ang in (0, 90) else "right"
        key = f"{self.fname}:l:{name}:{x}:{y}"
        if glob:
            self.items.append([S("global_label"), name, [S("shape"), S(shape)],
                               [S("at"), x, y, ang], [S("fields_autoplaced")],
                               [S("effects"), [S("font"), [S("size"), 1.27, 1.27]], [S("justify"), S(just)]],
                               [S("uuid"), U(key)],
                               _prop("Intersheetrefs", "${INTERSHEET_REFS}", x, y, hide=True)])
        else:
            self.items.append([S("label"), name, [S("at"), x, y, ang], [S("fields_autoplaced")],
                               [S("effects"), [S("font"), [S("size"), 1.27, 1.27]],
                                [S("justify"), S(just), S("bottom")]],
                               [S("uuid"), U(key)]])

    def no_connect(self, x, y):
        self.items.append([S("no_connect"), [S("at"), x, y], [S("uuid"), U(f"{self.fname}:nc:{x}:{y}")]])

    def _need(self, lib_id):
        if lib_id not in self.libs:
            self.libs[lib_id] = lib_symbol(lib_id)
        return self.libs[lib_id]

    def _instance(self, lib_id, ref, value, x, y, rot, props, pins, dnp=False, fp="",
                  ref_at=None, val_at=None, hide_ref=False, hide_val=False, in_bom=True):
        key = f"{self.fname}:s:{ref}"
        node = [S("symbol"), [S("lib_id"), lib_id], [S("at"), x, y, rot], [S("unit"), 1],
                [S("in_bom"), S("yes" if in_bom else "no")], [S("on_board"), S("yes")],
                [S("dnp"), S("yes" if dnp else "no")], [S("uuid"), U(key)]]
        rx, ry = ref_at or (x + 2.54, y - 1.27)
        vx, vy = val_at or (x + 2.54, y + 1.27)
        node.append(_prop("Reference", ref, rx, ry, hide=hide_ref, justify="left"))
        node.append(_prop("Value", value, vx, vy, hide=hide_val, justify="left"))
        node.append(_prop("Footprint", fp, x, y, hide=True))
        node.append(_prop("Datasheet", props.pop("Datasheet", "~"), x, y, hide=True))
        for k, v in props.items():
            node.append(_prop(k, v, x, y, hide=True))
        for num in sorted({p[0] for p in pins}):
            node.append([S("pin"), num, [S("uuid"), U(key + ":p:" + num)]])
        node.append([S("instances"), [S("project"), PROJECT,
                                      [S("path"), self.inst_path, [S("reference"), ref], [S("unit"), 1]]]])
        self.items.append(node)

    def power(self, net, x, y, o):
        _pwr_count[0] += 1
        ref = "#PWR%03d" % _pwr_count[0]
        lib_id = POWER_LIB[net]
        self._need(lib_id)
        rot = (POWER_ROT_DOWN if net == "GND" else POWER_ROT_UP)[o]
        tx, ty = x + 1.27 * o[0], y + 1.27 * o[1]
        vx, vy = (x, y + 5.08) if (net == "GND" and o == (0, 1)) else (x + 2.54 * o[0] + 1.27, y + 3.81 * o[1] - 0.5)
        self._instance(lib_id, ref, net, x, y, rot, {}, [("1",)], hide_ref=True,
                       ref_at=(tx, ty), val_at=(vx, vy), in_bom=False)

    def pwr_flag(self, net, x, y):
        """PWR_FLAG tells ERC this net is driven (connector/pass-through supplies)."""
        x, y = g(x), g(y)
        _pwr_count[0] += 1
        self._need("power:PWR_FLAG")
        self._instance("power:PWR_FLAG", "#FLG%03d" % _pwr_count[0], "PWR_FLAG", x, y, 0, {}, [("1",)],
                       hide_ref=True, val_at=(x, y - 3.81), in_bom=False)
        self.wire(x, y, x, y + 5.08)
        self.power(net, x, y + 5.08, (0, 1) if net == "GND" else (0, -1))

    # -- the main helper
    def part(self, lib_id, ref, value, x, y, nets, fp="", props=None, dnp=False, stub=2.54,
             ref_at=None, val_at=None, hide_val=False, in_bom=True):
        x, y = g(x), g(y)
        symdef = self._need(lib_id)
        pins = pin_table(symdef)
        props = dict(props or {})
        seen = {}
        for num, px, py, ang, ptype, hidden in pins:
            spec = nets.get(num)
            loc = (round(x + px, 3), round(y - py, 3))
            if spec is None:
                if ptype == "no_connect" or hidden:
                    continue
                raise ValueError(f"{ref}: pin {num} has no net")
            if loc in seen:
                if seen[loc] != spec:
                    raise ValueError(f"{ref}: stacked pin {num} net mismatch")
                continue
            seen[loc] = spec
            self.nets.setdefault(spec, []).append(f"{ref}.{num}")
            if spec == "X":           # connected by a wire drawn by the caller
                continue
            if spec == "NC":
                self.no_connect(*loc)
                continue
            o = OUTWARD[ang]
            ex, ey = round(loc[0] + o[0] * stub, 3), round(loc[1] + o[1] * stub, 3)
            self.wire(loc[0], loc[1], ex, ey)
            if spec in POWER_LIB:
                if o[1] == 0:   # sideways pin: bend so the power symbol stands upright
                    d = (0, 1) if spec == "GND" else (0, -1)
                    bx, by = ex, round(ey + d[1] * 2.54, 3)
                    self.wire(ex, ey, bx, by)
                    self.power(spec, bx, by, d)
                else:
                    self.power(spec, ex, ey, o)
            elif spec.startswith("G:"):
                self.label(spec[2:], ex, ey, o, glob=True)
            elif spec.startswith("L:"):
                self.label(spec[2:], ex, ey, o)
            else:
                raise ValueError(spec)
        self._instance(lib_id, ref, value, x, y, 0, props, pins, dnp=dnp, fp=fp,
                       ref_at=ref_at, val_at=val_at, hide_val=hide_val, in_bom=in_bom)

    # -- output
    def sheet_symbol(self, x, y, w, h, name):
        return [S("sheet"), [S("at"), x, y], [S("size"), w, h], [S("fields_autoplaced")],
                [S("stroke"), [S("width"), 0.1524], [S("type"), S("solid")]],
                [S("fill"), [S("color"), 0, 0, 0, 0.0]],
                [S("uuid"), self.sheet_uuid],
                _prop("Sheetname", name, x, y - 0.7, justify="left bottom"),
                _prop("Sheetfile", self.fname, x, y + h + 0.6, justify="left top"),
                [S("instances"), [S("project"), PROJECT, [S("path"), f"/{ROOT_UUID}", [S("page"), str(self.page)]]]]]

    def write(self, extra=()):
        tb = [S("title_block"), [S("title"), self.title], [S("date"), DATE], [S("rev"), REV],
              [S("company"), "RITHESH LED Display"]]
        for i, c in enumerate(self.comments, 1):
            tb.append([S("comment"), i, c])
        doc = [S("kicad_sch"), [S("version"), 20230121], [S("generator"), S("eeschema")],
               [S("uuid"), ROOT_UUID if self.path == "/" else self.uuid],
               [S("paper"), self.paper], tb,
               [S("lib_symbols")] + list(self.libs.values())]
        doc += self.items + list(extra)
        if self.path == "/":
            doc.append([S("sheet_instances"), [S("path"), "/", [S("page"), "1"]]])
        with open(os.path.join(OUT, self.fname), "w") as f:
            f.write(sexp.dump(doc) + "\n")


# ---------------------------------------------------------------- footprints / values
R0402 = "Resistor_SMD:R_0402_1005Metric"
C0402 = "Capacitor_SMD:C_0402_1005Metric"
C0603 = "Capacitor_SMD:C_0603_1608Metric"
C0805 = "Capacitor_SMD:C_0805_2012Metric"
CP63 = "Capacitor_SMD:CP_Elec_6.3x7.7"
TP_FP = "TestPoint:TestPoint_Pad_D1.5mm"
SW_FP = "Button_Switch_SMD:SW_Push_1P1T_XKB_TS-1187A"


# JLCPCB/LCSC parts chosen in the component-placement step (in stock 2026-09-23).
LCSC = {
    "5.1k 1%": "C25905", "100k 1%": "C25741", "100k": "C25741", "39k 1%": "C25783",
    "750R 1%": "C25132", "10k": "C25744", "33R": "C25105", "22R": "C25092", "499R": "C4125",
    "100nF": "C1525", "100nF 16V X7R": "C1525", "1uF 25V X5R": "C52923",
    "4.7uF 16V X5R": "C19666", "10uF 25V X5R": "C15850",
}


def R(sh, ref, val, x, y, n1, n2, **kw):
    props = dict(kw.pop("props", {}) or {})
    props.setdefault("LCSC", LCSC[val])
    sh.part("Device:R", ref, val, x, y, {"1": n1, "2": n2}, fp=R0402, props=props, **kw)


def C(sh, ref, val, x, y, n1, n2, fp=C0402, **kw):
    props = dict(kw.pop("props", {}) or {})
    if val in LCSC:
        props.setdefault("LCSC", LCSC[val])
    sh.part("Device:C", ref, val, x, y, {"1": n1, "2": n2}, fp=fp, props=props, **kw)


def TP(sh, ref, name, x, y, net):
    """Bare copper test pad: not a purchased part, so it is left out of the parts list."""
    sh.part("Connector:TestPoint", ref, name, x, y, {"1": net}, fp=TP_FP,
            ref_at=(x + 1.27, y - 3.81), val_at=(x + 1.27, y - 1.8), in_bom=False)


# ---------------------------------------------------------------- sheet 2: power
def power_sheet():
    sh = Sheet("power.kicad_sch", "Power input, protection and 3.3 V supply", "A3", 2,
               ("USB-C in -> protection -> eFuse -> +5V -> 3.3 V regulator",
                "Values: requirements.md (2026-09-23)"))

    sh.box(15, 25, 135, 175, "1. USB-C power input")
    sh.part("Connector:USB_C_Receptacle_USB2.0_16P", "J1", "USB-C (TYPE-C-31-M-12)", 45, 90,
            {"A4": "VBUS", "A9": "VBUS", "B4": "VBUS", "B9": "VBUS",
             "A1": "GND", "A12": "GND", "B1": "GND", "B12": "GND", "S1": "GND",
             "A5": "G:CC1", "B5": "G:CC2", "A6": "G:USB_DP", "B6": "G:USB_DP",
             "A7": "G:USB_DN", "B7": "G:USB_DN", "A8": "NC", "B8": "NC"},
            fp="rithesh_fp:USB_C_Receptacle_HRO_TYPE-C-31-M-12",
            props={"LCSC": "C165948", "Rating": "5 A power pins"},
            ref_at=(33, 63.5), val_at=(33, 67.5))
    R(sh, "R1", "5.1k 1%", 95, 125, "G:CC1", "GND")
    R(sh, "R2", "5.1k 1%", 110, 125, "G:CC2", "GND")
    sh.pwr_flag("VBUS", 100, 45)
    sh.pwr_flag("GND", 120, 45)
    sh.text("CC1/CC2: the two 5.1k resistors tell the charger\n"
            "'send 5 V'. The same lines go to the ESP32 (GPIO0/1)\n"
            "so charger strength can be measured later.\n"
            "SBU pins are not used. Shield goes to GND.\n"
            "PWR_FLAG symbols only tell the electrical check\n"
            "that VBUS and GND are supplied by the connector.", 18, 145, size=1.5)

    sh.box(140, 25, 225, 175, "2. Surge + static protection")
    sh.part("Diode:SMAJ5.0A", "D1", "SMAJ5.0A", 165, 60, {"1": "VBUS", "2": "GND"},
            fp="Diode_SMD:D_SMA", props={"LCSC": "C2925443"},
            ref_at=(168, 56), val_at=(160, 66))
    sh.part("Power_Protection:USBLC6-2SC6", "U1", "USBLC6-2SC6", 185, 100,
            {"1": "G:USB_DP", "6": "G:USB_DP", "3": "G:USB_DN", "4": "G:USB_DN",
             "5": "VBUS", "2": "GND"},
            fp="Package_TO_SOT_SMD:SOT-23-6", props={"LCSC": "C7519"},
            ref_at=(188, 92), val_at=(188, 112))
    C(sh, "C1", "4.7uF 16V X5R", 160, 130, "VBUS", "GND", fp=C0603)
    sh.text("D1 (surge diode): stays off at 5 V, clamps spikes\n"
            "above ~6.4 V (up to ~9.2 V in a big surge).\n"
            "Pin 1 = cathode (band) to VBUS.\n"
            "U1: guards the USB data lines against static.\n"
            "C1 4.7uF: the only capacitor the charger sees at\n"
            "plug-in (~5 uF total; USB-C allows 1-10 uF).", 143, 145, size=1.5)

    sh.box(230, 25, 330, 175, "3. eFuse: soft start, current limit, 5.7 V clamp")
    sh.part("rithesh:TPS259531", "U2", "TPS259531DSGR", 280, 70,
            {"3": "VBUS", "4": "VBUS", "2": "L:EN_UVLO", "1": "L:DVDT",
             "5": "+5V", "6": "L:~{FLT}", "7": "L:ILM", "8": "GND", "9": "GND"},
            fp=CUSTOM["TPS259531"][7][2], props={"LCSC": "C2155674"},
            ref_at=(270, 60), val_at=(270, 80.5))
    R(sh, "R4", "100k 1%", 245, 110, "VBUS", "L:EN_UVLO")
    R(sh, "R5", "39k 1%", 258, 110, "L:EN_UVLO", "GND")
    C(sh, "C2", "100nF 16V X7R", 271, 110, "L:DVDT", "GND")
    R(sh, "R3", "750R 1%", 290, 110, "L:ILM", "GND")
    R(sh, "R6", "100k", 305, 110, "+3V3", "L:~{FLT}")
    sh.text("R4/R5: turn on above ~4.3 V, off below ~3.9 V;\n"
            "keeps EN under its 7 V limit.\n"
            "C2 100nF: output ramps ~0.42 V/ms (~12 ms),\n"
            "plug-in current <= ~0.6 A.\n"
            "R3 750R: current limit 2.49-2.90 A. ILM voltage\n"
            "~0.21 V per amp (test pad TP8).\n"
            "~FLT: low = fault (open drain, R6 pull-up).\n"
            "Faults restart automatically every ~93 ms.", 233, 132, size=1.5)

    sh.box(335, 25, 405, 175, "4. 3.3 V regulator")
    sh.part("rithesh:TLV76733DRV", "U3", "TLV76733DRVR", 372, 70,
            {"6": "+5V", "4": "+5V", "1": "+3V3", "2": "+3V3", "3": "GND", "5": "GND", "7": "GND"},
            fp=CUSTOM["TLV76733DRV"][7][2], props={"LCSC": "C2848334"},
            ref_at=(365, 62.5), val_at=(365, 78))
    C(sh, "C7", "10uF 25V X5R", 352, 110, "+5V", "GND", fp=C0805)
    C(sh, "C8", "10uF 25V X5R", 392, 110, "+3V3", "GND", fp=C0805)
    sh.text("5 V in, 3.3 V out for the ESP32 (up to 1 A).\n"
            "EN tied to +5V: always on.\n"
            "SNS connects to the 3.3 V output.\n"
            "C7/C8: 10uF ceramic in and out.", 338, 132, size=1.5)

    sh.box(15, 185, 240, 250, "5. Test pads (for prototype measurements)")
    tps = [("TP1", "5V_IN", "VBUS"), ("TP2", "5V_OUT", "+5V"), ("TP3", "3V3", "+3V3"),
           ("TP4", "GND", "GND"), ("TP5", "GND", "GND"), ("TP7", "FLT", "L:~{FLT}"),
           ("TP8", "ILM", "L:ILM")]
    for i, (ref, name, net) in enumerate(tps):
        TP(sh, ref, name, 30 + i * 30, 215, net)
    sh.text("TP6 (LED data) is on the 'LED signal driver' sheet.", 18, 240, size=1.5)
    return sh


# ---------------------------------------------------------------- sheet 3: ESP32
def mcu_sheet():
    sh = Sheet("mcu.kicad_sch", "ESP32-C3 Wi-Fi controller and buttons", "A3", 3,
               ("ESP32-C3-MINI-1-N4 running WLED",
                "U4 footprint: official Espressif land pattern"))
    sh.box(140, 25, 290, 185, "1. ESP32-C3-MINI-1 module")
    sh.part("rithesh:ESP32-C3-MINI-1", "U4", "ESP32-C3-MINI-1-N4", 215, 100,
            {"3": "+3V3", "8": "L:EN", "12": "G:CC1", "13": "G:CC2", "5": "L:IO2_STRAP",
             "6": "NC", "18": "NC", "19": "L:BTN_ONOFF", "20": "NC", "21": "NC",
             "22": "L:IO8_STRAP", "23": "L:BOOT", "16": "G:LED_DATA_3V3",
             "26": "L:USB_DN_MCU", "27": "L:USB_DP_MCU", "30": "L:U0RXD", "31": "L:U0TXD",
             **{str(n): "GND" for n in [1, 2, 11, 14] + list(range(36, 54))}},
            fp=CUSTOM["ESP32-C3-MINI-1"][7][2], props={"LCSC": "C2838502"},
            ref_at=(200, 75), val_at=(200, 125))
    sh.text("GPIO0/GPIO1: USB-C CC1/CC2 sensing (ADC1).\n"
            "GPIO10: LED data (3.3 V) to the level shifter.\n"
            "GPIO18/19: built-in USB, through R14/R15 (box 5).\n"
            "GPIO20/21: serial debug pads (box 6).\n"
            "GPIO3, 4, 6, 7: not used (X marks).\n"
            "The antenna end of the module must sit at the board\n"
            "edge with no copper underneath (layout rule).", 143, 150, size=1.5)

    sh.box(15, 25, 135, 90, "2. 3.3 V decoupling")
    C(sh, "C9", "10uF 25V X5R", 50, 55, "+3V3", "GND", fp=C0805)
    C(sh, "C10", "100nF", 80, 55, "+3V3", "GND")
    sh.text("Place C9/C10 right next to the module's 3V3 pin\n(Espressif reference design).", 18, 75, size=1.5)

    sh.box(15, 95, 135, 185, "3. Enable delay + RESET button")
    R(sh, "R7", "10k", 40, 120, "+3V3", "L:EN")
    C(sh, "C11", "1uF 25V X5R", 60, 120, "L:EN", "GND")
    sh.part("Switch:SW_Push", "SW1", "RESET", 95, 120, {"1": "L:EN", "2": "GND"}, fp=SW_FP, props={"LCSC": "C318884"},
            ref_at=(90, 114), val_at=(90, 125))
    sh.text("R7 + C11 (10k, 1uF): the chip starts only after\n"
            "3.3 V is steady (Espressif recommendation).\n"
            "SW1 RESET: pulls EN low to restart the chip.\n"
            "Silkscreen label: RESET", 18, 150, size=1.5)

    sh.box(295, 25, 405, 185, "4. BOOT, ON/OFF buttons + boot pins")
    R(sh, "R8", "10k", 310, 55, "+3V3", "L:BOOT")
    sh.part("Switch:SW_Push", "SW2", "BOOT", 345, 58, {"1": "L:BOOT", "2": "GND"}, fp=SW_FP, props={"LCSC": "C318884"},
            ref_at=(340, 52), val_at=(340, 63))
    R(sh, "R11", "10k", 310, 95, "+3V3", "L:BTN_ONOFF")
    sh.part("Switch:SW_Push", "SW3", "ON/OFF", 345, 98, {"1": "L:BTN_ONOFF", "2": "GND"}, fp=SW_FP, props={"LCSC": "C318884"},
            ref_at=(340, 92), val_at=(340, 103))
    R(sh, "R9", "10k", 310, 130, "+3V3", "L:IO8_STRAP")
    R(sh, "R10", "10k", 330, 130, "+3V3", "L:IO2_STRAP")
    sh.box(15, 190, 200, 250, "5. USB data line resistors (close to U4)")
    R(sh, "R14", "22R", 40, 215, "G:USB_DP", "L:USB_DP_MCU")
    R(sh, "R15", "22R", 60, 215, "G:USB_DN", "L:USB_DN_MCU")
    C(sh, "C13", "DNP", 85, 215, "L:USB_DP_MCU", "GND", dnp=True)
    C(sh, "C14", "DNP", 105, 215, "L:USB_DN_MCU", "GND", dnp=True)
    sh.text("R14/R15 22R: Espressif recommends reserving series\n"
            "resistors (start at 22-33R) on USB D+/D-, near the chip.\n"
            "C13/C14: spare capacitor spots to GND,\n"
            "NOT FITTED (Espressif: initially unpopulated).\n"
            "Change only if USB proves unreliable.", 120, 200, size=1.5)
    sh.box(205, 190, 405, 250, "6. Serial debug pads (UART0)")
    R(sh, "R16", "499R", 235, 215, "L:U0TXD", "L:U0TXD_TP")
    TP(sh, "TP9", "IO20_RX", 260, 215, "L:U0RXD")
    TP(sh, "TP10", "IO21_TX", 280, 215, "L:U0TXD_TP")
    sh.text("Connect a 3.3 V USB-serial adapter here if USB\n"
            "programming or WLED ever fails: adapter TX -> TP9,\n"
            "adapter RX -> TP10, adapter GND -> a GND pad.\n"
            "R16 499R on TX: Espressif recommendation\n"
            "(suppresses harmonics). Never connect 5 V here.", 298, 200, size=1.5)
    sh.text("SW2 BOOT (GPIO9): hold while pressing RESET to load\n"
            "new software (rescue button). Label: BOOT\n"
            "SW3 ON/OFF (GPIO5): WLED button 0, short press =\n"
            "display on/off. Label: ON/OFF\n"
            "R8/R11: keep the buttons reading 'not pressed'.\n"
            "R9/R10: hold boot pins GPIO8/GPIO2 high so the\n"
            "chip always starts normally.", 298, 150, size=1.5)
    return sh


# ---------------------------------------------------------------- sheet 4: LED driver
def driver_sheet():
    sh = Sheet("led_driver.kicad_sch", "LED signal driver (3.3 V -> 5 V)", "A4", 4,
               ("74AHCT1G125: 3.3 V -> 5 V LED data",))
    sh.box(15, 25, 280, 150, "LED data level shifter")
    sh.part("74xGxx:74AHCT1G125", "U5", "74AHCT1G125", 140, 85,
            {"2": "G:LED_DATA_3V3", "1": "GND", "5": "+5V", "3": "GND", "4": "L:LED_DATA_5V"},
            fp="Package_TO_SOT_SMD:SOT-23-5", props={"LCSC": "C7484", "MPN": "SN74AHCT1G125DBVR"},
            ref_at=(143, 70), val_at=(143, 98))
    R(sh, "R12", "10k", 100, 97, "G:LED_DATA_3V3", "GND")
    C(sh, "C12", "100nF", 115, 60, "+5V", "GND")
    R(sh, "R13", "33R", 175, 100, "L:LED_DATA_5V", "G:LED_DIN")
    TP(sh, "TP6", "LED_DIN", 205, 88, "G:LED_DIN")
    sh.text("U5 runs from +5V, so its output is a clean 5 V signal for the LEDs.\n"
            "Pin 1 (~OE, enable) is tied to GND = always on.\n"
            "R12 10k pull-down: holds the signal low while the ESP32 starts,\n"
            "so the LEDs do not flash random colors at plug-in.\n"
            "R13 33R: small series resistor that damps ringing on the data line\n"
            "(standard practice, added in the schematic step).\n"
            "C12 100nF: local decoupling for U5.\n"
            "TP6: scope the LED timing here (timing pre-check / prototype test).",
            18, 122, size=1.5)
    return sh


# ---------------------------------------------------------------- sheet 5: LEDs
LETTERS = [("R", 18), ("I", 11), ("T", 11), ("H", 17), ("E", 18), ("S", 15), ("H", 17)]


def leds_sheet():
    sh = Sheet("leds.kicad_sch", "107 LEDs spelling RITHESH", "A1", 5,
               ("XL-5050RGBC-2812B (C2843785) - PROVISIONAL",
                "Data chain: LED_DIN -> LED1 -> ... -> LED107"))
    n = 0
    y = 55
    for letter, count in LETTERS:
        sh.text(f"Letter {letter}: LED{n + 1} to LED{n + count} ({count} LEDs)", 25, y - 22, size=2.2, bold=True)
        for i in range(count):
            n += 1
            cx = g(40.64 + i * 40.64)
            din = "G:LED_DIN" if n == 1 else ("L:CHAIN_%d" % (n - 1) if i == 0 else "X")
            dout = "NC" if n == 107 else ("L:CHAIN_%d" % n if i == count - 1 else "X")
            sh.part("rithesh:XL-5050RGBC-2812B", f"LED{n}", "XL-5050RGBC-2812B", cx, y,
                    {"4": din, "2": dout, "1": "+5V", "3": "GND"},
                    fp="rithesh_fp:LED_XL-5050RGBC-2812B",
                    props={"LCSC": "C2843785", "Status": "PROVISIONAL"},
                    ref_at=(cx + 2.54, y - 12.7), val_at=(cx + 2.54, y + 12.7), hide_val=True)
            if i < count - 1:
                sh.wire(cx + 7.62, g(y), g(40.64 + (i + 1) * 40.64) - 7.62, g(y))
        y += 50

    ytop = y - 10
    sh.box(15, ytop, 790, ytop + 105, "Per-LED capacitors C101-C207 (100nF each)")
    sh.text("C101 sits next to LED1, C102 next to LED2, ... C207 next to LED107 on the PCB.\n"
            "Each one steadies its own LED's 5 V supply.", 25, ytop + 8, size=1.8)
    for k in range(107):
        row, col = divmod(k, 36)
        C(sh, f"C{101 + k}", "100nF", 40 + col * 20, ytop + 35 + row * 30, "+5V", "GND",
          ref_at=(40 + col * 20 + 1.5, ytop + 35 + row * 30 - 2), hide_val=True)

    yb = ytop + 115
    sh.box(15, yb, 420, yb + 70, "Bulk capacitors on the LED power rail (220uF 16V, C72496)")
    spots = [("C301", "start of LED chain", False), ("C302", "one-third along", True),
             ("C303", "two-thirds along", True), ("C304", "far end", False)]
    for i, (ref, where, dnp) in enumerate(spots):
        x = 50 + i * 90
        sh.part("Device:C_Polarized", ref, "220uF 16V" + (" DNP" if dnp else ""), x, yb + 38,
                {"1": "+5V", "2": "GND"}, fp=CP63, props={"LCSC": "C72496"}, dnp=dnp)
        sh.text(("NOT FITTED (DNP) - " if dnp else "FITTED - ") + where, x - 20, yb + 55, size=1.5)
    sh.text("+ side to +5V. Print a '+' mark next to every footprint. Fit the two DNP\n"
            "capacitors only if prototype testing shows rail dips or capacitor heating.",
            430, yb + 10, size=1.8)
    return sh


# ---------------------------------------------------------------- root
def root_sheet(children):
    root = Sheet(PROJECT + ".kicad_sch", "RITHESH LED Display - overview", "A3", 1,
                 ("107 RGB LEDs spelling RITHESH, Wi-Fi (WLED)",
                  "Prototype rev - see schematic-review.md",
                  "Made by tools/generate_schematic.py"))
    root.text("RITHESH LED Display - schematic overview", 20, 20, size=4, bold=True)
    root.text("How to read this schematic:\n"
              "- Each box below is a sheet. Double-click it in KiCad (or turn the PDF page) to open it.\n"
              "- Parts connect by NAME: two labels with the same text are the same wire.\n"
              "- Flag-shaped labels (e.g. USB_DP) connect across sheets. Plain labels stay on one sheet.\n"
              "- +5V, +3V3, VBUS and GND symbols connect everywhere.\n"
              "- An X on a pin means 'intentionally not connected'.",
              20, 32, size=2)
    sheets = [("Power input, protection, 3.3 V", 20, 90), ("ESP32-C3 controller + buttons", 120, 90),
              ("LED signal driver", 220, 90), ("107 LEDs (RITHESH)", 320, 90)]
    extra = []
    for sh, (name, x, y) in zip(children, sheets):
        extra.append(sh.sheet_symbol(x, y, 80, 30, name))
    root.text("Power flow:  USB-C (VBUS, 5 V) -> surge diode + ESD -> eFuse TPS259531 -> +5V\n"
              "             +5V -> 107 LEDs + level shifter;   +5V -> TLV76733 -> +3V3 -> ESP32\n\n"
              "Signal flow: ESP32 GPIO10 (LED_DATA_3V3) -> 74AHCT1G125 -> LED_DIN -> LED1 -> ... -> LED107\n\n"
              "Nets that cross sheets: CC1, CC2, USB_DP, USB_DN, LED_DATA_3V3, LED_DIN\n\n"
              "Open items (see schematic-review.md): LED part is PROVISIONAL until the timing pre-check.",
              20, 140, size=2)
    root.write(extra)


def project_files():
    # Merge into an existing project file so board design rules saved by KiCad are kept.
    import json
    pro_path = os.path.join(OUT, PROJECT + ".kicad_pro")
    pro = json.load(open(pro_path)) if os.path.exists(pro_path) else {}
    pro.setdefault("meta", {}).update({"filename": PROJECT + ".kicad_pro", "version": 1})
    pro["sheets"] = [[ROOT_UUID, ""]]
    with open(pro_path, "w") as f:
        json.dump(pro, f, indent=2)
        f.write("\n")
    lib = [S("kicad_symbol_lib"), [S("version"), 20220914], [S("generator"), S("rithesh_gen")]]
    lib += [copy.deepcopy(s) for s in CUSTOM.values()]
    with open(os.path.join(OUT, "rithesh.kicad_sym"), "w") as f:
        f.write(sexp.dump(lib) + "\n")
    with open(os.path.join(OUT, "sym-lib-table"), "w") as f:
        f.write('(sym_lib_table\n  (version 7)\n  (lib (name "rithesh")(type "KiCad")'
                '(uri "${KIPRJMOD}/rithesh.kicad_sym")(options "")(descr "RITHESH project symbols"))\n)\n')


def main():
    children = [power_sheet(), mcu_sheet(), driver_sheet(), leds_sheet()]
    for sh in children:
        sh.write()
    root_sheet(children)
    project_files()
    print("wrote", len(children) + 1, "sheets to", OUT)


if __name__ == "__main__":
    main()