#!/usr/bin/env python3 """Export and validate the checked-in schematic; never regenerates the design.""" import json from pathlib import Path import re import subprocess import xml.etree.ElementTree as ET ROOT = Path(__file__).resolve().parent OUT = ROOT / "validation" SCHEMATIC = ROOT / "serial-carrier.kicad_sch" def run(*args): subprocess.run(args, check=True, cwd=ROOT) run("kicad-cli", "sch", "export", "netlist", "--format", "kicadxml", "-o", str(OUT / "serial-carrier.xml"), str(SCHEMATIC)) run("kicad-cli", "sch", "erc", "--format", "json", "--exit-code-violations", "-o", str(OUT / "erc.json"), str(SCHEMATIC)) root = ET.parse(OUT / "serial-carrier.xml").getroot() nets = {net.get("name").removeprefix("/"): net.findall("node") for net in root.findall("nets/net")} members = {name: sorted((node.get("ref"), node.get("pin")) for node in nodes) for name, nodes in nets.items()} components = {comp.get("ref"): comp for comp in root.findall("components/comp")} assert set(components) == {"U1", "U2", "DS1", "SW1", "SW2", "SW3", "R1", "R2", "R3"} # Independent firmware-to-module contract, not imported from the schematic generator. contracts = [ ("RS_TX", "RS232_TX_GPIO", "TX", "5"), ("RS_RX", "RS232_RX_GPIO", "RX", "4"), ("RS_RTS", "RS232_RTS_GPIO", "RTS", "8"), ("RS_CTS", "RS232_CTS_GPIO", "CTS", "9"), ("RS_DTR", "RS232_DTR_GPIO", "DTR", "6"), ("RS_DSR", "RS232_DSR_GPIO", "DSR", "7"), ("RS_DCD", "RS232_DCD_GPIO", "DCD", "3"), ("RS_RI", "RS232_RI_GPIO", "RI", "10"), ("RS_VALID", "RS232_VALID_GPIO", "VLD", "11"), ("RS_OFF_N", "RS232_FORCE_OFF_N_GPIO", "OFF_N", "12"), ("OLED_SDA", "LOCAL_UI_DISPLAY_SDA_GPIO", "SDA", "4"), ("OLED_SCL", "LOCAL_UI_DISPLAY_SCL_GPIO", "SCL", "3"), ("BTN_PREVIOUS", "LOCAL_UI_BUTTON_PREVIOUS_GPIO", None, None), ("BTN_SELECT", "LOCAL_UI_BUTTON_SELECT_GPIO", None, None), ("BTN_NEXT", "LOCAL_UI_BUTTON_NEXT_GPIO", None, None), ] firmware = ROOT.parents[1].joinpath("src/board_pins.h").read_text() for net, macro, peripheral_name, peripheral_pin in contracts: match = re.search(r"^#define\s+" + re.escape(macro) + r"\s+GPIO_NUM_(\d+)\s*$", firmware, re.MULTILINE) assert match, macro gpio = "GPIO" + match.group(1) mcu = [node for node in nets[net] if node.get("ref") == "U1"] assert len(mcu) == 1 and mcu[0].get("pinfunction") == gpio + "_" + mcu[0].get("pin"), (net, gpio) if peripheral_name: ref = "DS1" if net.startswith("OLED_") else "U2" node = [node for node in nets[net] if node.get("ref") == ref] assert len(node) == 1 and node[0].get("pin") == peripheral_pin assert node[0].get("pinfunction") == peripheral_name + "_" + peripheral_pin assert len(nets[net]) == 2, (net, members[net]) for i, net in enumerate(("BTN_PREVIOUS", "BTN_SELECT", "BTN_NEXT"), 1): assert len(nets[net]) == 3 assert (f"SW{i}", "1") in members[net] and (f"R{i}", "2") in members[net] assert components[f"SW{i}"].findtext("value") == "B3F-1000" assert components[f"R{i}"].findtext("value") == "2.2k 1%" assert members["+3V3"] == sorted([("U1","1"),("U1","2"),("U2","1"),("DS1","2"),("R1","1"),("R2","1"),("R3","1")]) assert members["GND"] == sorted([("U1","22"),("U1","23"),("U1","43"),("U1","44"),("U2","2"),("DS1","1"),("SW1","2"),("SW2","2"),("SW3","2")]) active = {name for name in nets if not name.startswith("unconnected-")} assert active == {"+3V3", "GND"} | {row[0] for row in contracts} for name, nodes in nets.items(): if name.startswith("unconnected-"): assert len(nodes) == 1 and nodes[0].get("ref") == "U1" # All 44 MCU header pins must occur exactly once, including deliberate no-connects. assert sorted(int(node.get("pin")) for nodes in nets.values() for node in nodes if node.get("ref") == "U1") == list(range(1,45)) # USB/UART0, 5 V and PSRAM-conflicting pins must never be carrier nets. for name in active: assert not any(node.get("ref") == "U1" and node.get("pin") in ("21","24","25","33","34","35","41","42") for node in nets[name]) # U2's physical male footprint must stay explicitly unresolved in this draft. assert not components["U2"].findtext("footprint") expected = json.loads((OUT / "expected-nets.json").read_text()) assert set(expected) == active for net, nodes in expected.items(): assert members[net] == sorted(tuple(node) for node in nodes), net report = json.loads((OUT / "erc.json").read_text()) assert not any(sheet["violations"] for sheet in report["sheets"]) run("kicad-cli", "sch", "export", "pdf", "-o", str(ROOT / "serial-carrier.pdf"), str(SCHEMATIC)) run("kicad-cli", "sch", "export", "svg", "-o", str(OUT) + "/", str(SCHEMATIC)) summary = "\n".join([ "Native schematic validation passed.", "KiCad ERC: zero reported errors/warnings; no project exclusions added.", "17 connected nets, 9 components; exact net memberships verified.", "15 firmware GPIO definitions independently checked against exported pin functions.", "All 44 MCU header pins accounted for; USB/UART0/5V/PSRAM pins unused on carrier.", "PDF and SVG exported successfully.", "Not validated: male-module physical mapping, power budget/path, OLED pull-ups,", "selected connector fit, PCB DRC/layout, procurement, or assembled hardware.", ]) + "\n" (OUT / "summary.txt").write_text(summary) print(summary)