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
"""Pin audit: symbols vs datasheet pin tables, symbols vs footprints, and connection rules.

Usage: python3 check_pins.py <netlist.xml>
The DATASHEET tables below are transcribed from the official datasheets (sources in comments).
"""
import os
import sys
import xml.etree.ElementTree as ET

import sexp

HW = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
FP_DIR = "/usr/share/kicad/footprints/"

# ---- datasheet pin tables (pin number -> function) ------------------------------
ESP = {"3": "3V3", "8": "EN", "5": "IO2", "6": "IO3", "12": "IO0", "13": "IO1", "16": "IO10",
       "18": "IO4", "19": "IO5", "20": "IO6", "21": "IO7", "22": "IO8", "23": "IO9",
       "26": "IO18", "27": "IO19", "30": "RXD0", "31": "TXD0"}
ESP.update({str(n): "GND" for n in [1, 2, 11, 14] + list(range(36, 54))})
ESP.update({str(n): "NC" for n in [4, 7, 9, 10, 15, 17, 24, 25, 28, 29, 32, 33, 34, 35]})
DATASHEET = {
    # Espressif ESP32-C3-MINI-1 datasheet v2.2, Table 3-1 (EPAD = pin 49, GND)
    "ESP32-C3-MINI-1": ESP,
    # TI TPS2595 SLVSE57C, Pin Functions, TPS2595x0/1 DSG; exposed PAD = GND (footprint pad 9)
    "TPS259531": {"1": "dVdt", "2": "EN/UVLO", "3": "IN", "4": "IN", "5": "OUT", "6": "FLT",
                  "7": "ILM", "8": "GND", "9": "GND"},
    # TI TLV767 SLVSE84D, Figure 5-2 DRV (fixed); thermal pad = GND (footprint pad 7)
    "TLV76733DRV": {"1": "OUT", "2": "SNS", "3": "GND", "4": "EN", "5": "GND", "6": "IN", "7": "GND"},
    # XINGLIGHT XL-5050RGBC-2812B datasheet, pin table (page 11)
    "XL-5050RGBC-2812B": {"1": "VDD", "2": "DOUT", "3": "GND", "4": "DIN"},
}
ALIASES = {"~{FLT}": "FLT", "PAD": "GND", "VSS": "GND", "DO": "DOUT", "DI": "DIN"}


def norm(name):
    name = ALIASES.get(name, name)
    return name.split("/")[0] if not name.startswith("EN/") else name


# ---- load netlist -----------------------------------------------------------------
root = ET.parse(sys.argv[1]).getroot()
pin_net, net_size = {}, {}
for net in root.iter("net"):
    name = net.get("name").split("/")[-1]
    nodes = list(net.iter("node"))
    for n in nodes:
        pin_net[(n.get("ref"), n.get("pin"))] = name
    net_size[name] = net_size.get(name, 0) + len(nodes)
comps = {c.get("ref"): (c.findtext("value"), c.findtext("footprint") or "") for c in root.iter("comp")}

errors, notes = [], []

# ---- 1. custom symbols vs datasheet ----------------------------------------------
lib = sexp.parse(open(os.path.join(HW, "rithesh.kicad_sym")).read())
for sym in sexp.find(lib, "symbol"):
    name = sym[1]
    pins = {}
    for unit in sexp.find(sym, "symbol"):
        for p in sexp.find(unit, "pin"):
            pins[sexp.find1(p, "number")[1]] = norm(sexp.find1(p, "name")[1])
    ds = DATASHEET[name]
    if set(pins) != set(ds):
        errors.append(f"{name}: pin numbers differ from datasheet: extra {set(pins) - set(ds)} missing {set(ds) - set(pins)}")
    for num, fn in ds.items():
        if num in pins and pins[num] != fn:
            errors.append(f"{name}: pin {num} is '{pins[num]}', datasheet says '{fn}'")
    notes.append(f"{name}: {len(ds)} pins match the datasheet table")

# ---- 2. symbol pins vs footprint pads --------------------------------------------
fp_cache = {}
for ref, (val, fp) in sorted(comps.items()):
    if not fp:
        errors.append(f"{ref}: no footprint assigned")
        continue
    lib_name, fp_name = fp.split(":")
    base = HW if lib_name == "rithesh_fp" else FP_DIR
    path = os.path.join(base, lib_name + ".pretty", fp_name + ".kicad_mod")
    if path not in fp_cache:
        mod = sexp.parse(open(path).read())
        fp_cache[path] = {str(p[1]) for p in sexp.find(mod, "pad") if str(p[1])}
    pads = fp_cache[path]
    sym_pins = {pin for (r, pin) in pin_net if r == ref}
    if sym_pins - pads:
        errors.append(f"{ref} ({fp_name}): symbol pins {sorted(sym_pins - pads)} have no footprint pad")
    if pads - sym_pins:
        errors.append(f"{ref} ({fp_name}): footprint pads {sorted(pads - sym_pins)} not in symbol")
notes.append("symbol pins = footprint pads for every part (incl. ESP32 and LED project footprints)")


# ---- 3. required connections -------------------------------------------------------
def want(ref, pin, net, why):
    got = pin_net.get((ref, pin))
    if got != net:
        errors.append(f"{ref}.{pin} ({why}) is on '{got}', expected '{net}'")


# power and ground pins
for pin in ["3", "4"]:
    want("U2", pin, "VBUS", "eFuse IN")
want("U2", "5", "+5V", "eFuse OUT")
want("U2", "8", "GND", "eFuse GND")
want("U2", "9", "GND", "eFuse exposed pad")
want("U3", "6", "+5V", "LDO IN")
want("U3", "4", "+5V", "LDO EN (always on)")
want("U3", "1", "+3V3", "LDO OUT")
want("U3", "2", "+3V3", "LDO SNS to OUT")
for pin, why in [("3", "LDO GND"), ("5", "LDO GND"), ("7", "LDO thermal pad")]:
    want("U3", pin, "GND", why)
want("U4", "3", "+3V3", "ESP32 3V3")
for n in [1, 2, 11, 14] + list(range(36, 54)):
    want("U4", str(n), "GND", "ESP32 GND/EPAD" if n == 49 else "ESP32 GND")
want("U5", "5", "+5V", "level shifter VCC")
want("U5", "3", "GND", "level shifter GND")
want("U5", "1", "GND", "level shifter OE (active low = enabled)")
for n in range(1, 108):
    want(f"LED{n}", "1", "+5V", "LED VDD")
    want(f"LED{n}", "3", "GND", "LED GND")

# USB-C connector: every electrical pin of the 16-pin receptacle
for pin in ["A4", "A9", "B4", "B9"]:
    want("J1", pin, "VBUS", "USB-C VBUS")
for pin in ["A1", "A12", "B1", "B12", "S1"]:
    want("J1", pin, "GND", "USB-C GND/shield")
want("J1", "A5", "CC1", "USB-C CC1")
want("J1", "B5", "CC2", "USB-C CC2")
for pin in ["A6", "B6"]:
    want("J1", pin, "USB_DP", "USB-C D+ (both plug orientations)")
for pin in ["A7", "B7"]:
    want("J1", pin, "USB_DN", "USB-C D- (both plug orientations)")

# USB protection chip (ST USBLC6-2SC6: 1/6 = I/O1, 3/4 = I/O2, 5 = VBUS, 2 = GND)
for pin in ["1", "6"]:
    want("U1", pin, "USB_DP", "USBLC6 I/O1 on D+")
for pin in ["3", "4"]:
    want("U1", pin, "USB_DN", "USBLC6 I/O2 on D-")
want("U1", "5", "VBUS", "USBLC6 VBUS reference")
want("U1", "2", "GND", "USBLC6 GND")
# D+ and D- reach the ESP32 on the right pins (IO19 = D+, IO18 = D-) via R14/R15
want("U4", "27", "USB_DP_MCU", "ESP32 IO19 = USB D+")
want("U4", "26", "USB_DN_MCU", "ESP32 IO18 = USB D-")

# surge diode: pin 1 = cathode (bar in symbol, band side/pad 1 of D_SMA) to VBUS
want("D1", "1", "VBUS", "TVS cathode")
want("D1", "2", "GND", "TVS anode")


# ---- 4. unused pins must be unconnected -------------------------------------------
def unused(ref, pin, why):
    net = pin_net.get((ref, pin))
    if net is None or net_size.get(net, 0) != 1:
        errors.append(f"{ref}.{pin} ({why}) should be unconnected but is on '{net}'")


for pin, why in [("6", "IO3"), ("18", "IO4"), ("20", "IO6"), ("21", "IO7")]:
    unused("U4", pin, f"ESP32 {why} spare")
for n in [4, 7, 9, 10, 15, 17, 24, 25, 28, 29, 32, 33, 34, 35]:
    unused("U4", str(n), "ESP32 NC pad")
unused("J1", "A8", "USB-C SBU1")
unused("J1", "B8", "USB-C SBU2")
unused("LED107", "2", "last LED data out")

# every other pin must be on a net with at least one other pin
allowed_single = {("U4", p) for p in ["6", "18", "20", "21", "4", "7", "9", "10", "15", "17", "24", "25",
                                      "28", "29", "32", "33", "34", "35"]} | {("J1", "A8"), ("J1", "B8"), ("LED107", "2")}
for key, net in pin_net.items():
    if net_size[net] == 1 and key not in allowed_single:
        errors.append(f"{key[0]}.{key[1]} is not connected to anything")

for n in notes:
    print("OK  " + n)
if errors:
    print("PROBLEMS:")
    for e in errors:
        print("  " + e)
    sys.exit(1)
print("OK  power/ground pins, exposed pads, USB-C pins, USBLC6, surge diode and unused pins all as intended")