AnasSarkiz/Game-Boy-Enhance

This code defines a hardware schematic featuring a CPU with associated power, clock, and interface components, including specialized chips, crystals, connectors, and passive components, all structured for circuit design and simulation.

Version
1.0.3
License
unset
Stars
0

scripts/audit-s4-imports.py

"""Check imported candidate packages, without claiming board-level DRC."""

import hashlib
import json
import math
import re
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
IMPORTS = ROOT / "s4/imports"


def attribute(pad, name):
    match = re.search(rf'\b{name}="([^"]+)"', pad)
    if match is None:
        raise ValueError(f"Missing {name} in imported pad")
    return match.group(1)


def check_package(filename, expected_pins):
    source = (IMPORTS / filename).read_text()
    pads = []
    for pad in re.findall(r"<(?:smtpad|platedhole)\b.*?/>", source, re.DOTALL):
        if attribute(pad, "shape") not in ["rect", "pill"] or "pcbRotation" in pad:
            raise ValueError("Audit requires an axis-aligned rectangular or pill pad")
        pin = re.search(r'portHints=\{\["pin(\d+)"(?:,"[^"]+")*\]\}', pad)
        if pin is None:
            raise ValueError("Imported pad has no unique numbered pin hint")
        is_plated_hole = pad.startswith("<platedhole")
        dimensions = {field: float(attribute(pad, field).removesuffix("mm")) for field in ["pcbX", "pcbY"]}
        dimensions.update({field: float(attribute(pad, f"outer{field.title()}" if is_plated_hole else field).removesuffix("mm"))
                           for field in ["width", "height"]})
        if is_plated_hole:
            for field in ["width", "height"]:
                drill = float(attribute(pad, f"hole{field.title()}").removesuffix("mm"))
                assert 0 < drill < dimensions[field], "Invalid plated slot or missing copper annulus"
        pads.append({"pin": int(pin.group(1)), **dimensions})
    if sorted(pad["pin"] for pad in pads) != list(range(1, expected_pins + 1)):
        raise ValueError(f"Missing or duplicated copper pad in {filename}")
    outline_match = re.search(r"<courtyardoutline outline=\{(\[.*?\])\}", source)
    if outline_match is None:
        raise ValueError(f"Missing imported courtyard in {filename}")
    outline = json.loads(outline_match.group(1))
    if outline[0] != outline[-1] or len(outline) != 5:
        raise ValueError("Expected a closed rectangular supplier courtyard")
    min_x, max_x = min(p["x"] for p in outline), max(p["x"] for p in outline)
    min_y, max_y = min(p["y"] for p in outline), max(p["y"] for p in outline)
    if any(p["x"] not in [min_x, max_x] or p["y"] not in [min_y, max_y] for p in outline):
        raise ValueError("Supplier courtyard is not rectangular")
    for pad in pads:
        if not (min_x <= pad["pcbX"] - pad["width"] / 2
                and max_x >= pad["pcbX"] + pad["width"] / 2
                and min_y <= pad["pcbY"] - pad["height"] / 2
                and max_y >= pad["pcbY"] + pad["height"] / 2):
            raise ValueError(f"Pad {pad['pin']} extends outside the supplier courtyard")
    for hole in re.findall(r"<hole\b.*?/>", source, re.DOTALL):
        radius = float(attribute(hole, "diameter").removesuffix("mm")) / 2
        x_mm = float(attribute(hole, "pcbX").removesuffix("mm"))
        y_mm = float(attribute(hole, "pcbY").removesuffix("mm"))
        assert min_x <= x_mm - radius < x_mm + radius <= max_x
        assert min_y <= y_mm - radius < y_mm + radius <= max_y
        for pad in pads:
            dx = max(abs(x_mm - pad["pcbX"]) - pad["width"] / 2, 0)
            dy = max(abs(y_mm - pad["pcbY"]) - pad["height"] / 2, 0)
            assert math.hypot(dx, dy) > radius, "Non-plated hole intersects copper pad"
    clearances = []
    for index, first in enumerate(pads):
        for second in pads[index + 1:]:
            dx = abs(first["pcbX"] - second["pcbX"]) - (first["width"] + second["width"]) / 2
            dy = abs(first["pcbY"] - second["pcbY"]) - (first["height"] + second["height"]) / 2
            if dx <= 0 and dy <= 0:
                raise ValueError(f"Copper bounds overlap: {first['pin']} and {second['pin']}")
            clearances.append(math.hypot(max(dx, 0), max(dy, 0)))
    return {"file": filename, "sha256": hashlib.sha256(source.encode()).hexdigest(),
            "pad_count": len(pads), "closed_courtyard_contains_pads": True,
            "pad_bounding_boxes_overlap": False,
            "minimum_pad_bound_clearance_mm": round(min(clearances), 6)}


def main():
    expected_pins = {"TLV62569PDDCR.tsx": 6, "ADV7513BSWZ.tsx": 65, "T113_S3.tsx": 129,
                     "CM4024M00008001.tsx": 4,
                     "HX_TYPE_C_16P_L8_35.tsx": 16, "USBLC6_2SC6.tsx": 6,
                     "ZDSD04GLGEAG.tsx": 8, "SN74AHC1G08DCKR.tsx": 5,
                     **{f"{name}.tsx": 2 for name in [
                         "CL10A106MA8NRNC", "CL05C100JB5NNNC", "CL21A226MAQNNNE", "CL05B104KB54PNC",
                         "NCD0805R1", "XRIM252012S2R2MBCA", "A_0402WGF1003TCE", "A_0402WGF4533TCE",
                         "A_0402WGF5101TCE", "A_0402WGF5102TCE", "CL05A225MQ5NSNC", "A_0402CG180J500NT",
                         "A_0603WAF2400T5E", "TSA010A2026B", "Q13FC13500004",
                         "JK_nSMD100_16", "A_0402WGF220JTCE",
                     ]}}
    report = {
        "scope": "candidate_import_geometry_only",
        "limitation": "No circuit connections, board placement, copper routing, solder-mask or assembly check",
        "packages": [check_package(path.name, expected_pins[path.name])
                     for path in sorted(IMPORTS.glob("*.tsx"))],
    }
    (ROOT / "s4/reports/import-geometry.json").write_text(json.dumps(report, indent=2) + "\n")
    print(json.dumps(report, indent=2))


if __name__ == "__main__":
    main()