krishnax12/t113-minimal-linux

Allwinner T113-S3 dual-Cortex-A7 SoC with integrated 128MB DDR3, multi-rail decoupling, 24MHz and 32.768kHz oscillators, filtering, and exposed test points.

Version
0.1.1
License
unset
Stars
0

scripts/check-gerbers.py

"""Parse exported copper/drills and compare drill geometry to Circuit JSON."""
import hashlib
import json
import sys
import warnings
import zipfile
from pathlib import Path

from gerbonara.excellon import ExcellonFile
from gerbonara.rs274x import GerberFile
from shapely.geometry import LineString, Point
from shapely.ops import unary_union

from routing_geometry import geom

circuit_path, archive_path = map(Path, sys.argv[1:3])
circuit = json.loads(circuit_path.read_text())
checks = []
diagnostics = []
parsed_layers = {}
drills = {"plated": [], "nonplated": []}
with zipfile.ZipFile(archive_path) as archive:
    for filename in archive.namelist():
        content = archive.read(filename).decode()
        with warnings.catch_warnings(record=True) as captured:
            warnings.simplefilter("always")
            if filename.endswith(".gbr"):
                layer = GerberFile.from_string(content)
                parsed_layers[filename] = len(layer.objects)
            elif filename.endswith(".drl"):
                plating = "nonplated" if "FileFunction,NonPlated" in content else "plated"
                assert f"FileFunction,{'NonPlated' if plating == 'nonplated' else 'Plated'},1,4," in content
                layer = ExcellonFile.from_string(content)
                for obj in layer.objects:
                    radius = obj.aperture.diameter / 2
                    if type(obj).__name__ == "Flash":
                        shape = Point(obj.x, obj.y).buffer(radius, quad_segs=24)
                    elif type(obj).__name__ == "Line":
                        shape = LineString([(obj.x1, obj.y1), (obj.x2, obj.y2)]).buffer(radius, quad_segs=24)
                    else:
                        raise ValueError(f"Unsupported drill object: {obj}")
                    drills[plating].append(shape)
            diagnostics.extend({"file": filename, "message": str(entry.message)} for entry in captured)

for filename in ["F_Cu.gbr", "In1_Cu.gbr", "In2_Cu.gbr", "B_Cu.gbr", "Edge_Cuts.gbr"]:
    checks.append({"id": filename + "_nonempty", "passed": parsed_layers.get(filename, 0) > 0})
for plating, record_types in [("plated", {"pcb_via", "pcb_plated_hole"}), ("nonplated", {"pcb_hole"})]:
    expected = unary_union([geom(record, drill=True) for record in circuit if record["type"] in record_types])
    actual = unary_union(drills[plating])
    missing = expected.difference(actual.buffer(.0002)).area
    extra = actual.difference(expected.buffer(.0002)).area
    checks.append({"id": plating + "_drill_geometry", "passed": missing < 1e-8 and extra < 1e-8, "missingAreaMm2": missing, "extraAreaMm2": extra})
report = {
    "passed": all(check["passed"] for check in checks),
    "checks": checks,
    "parsedGerberObjects": parsed_layers,
    "parserDiagnostics": diagnostics,
    "archiveSha256": hashlib.sha256(archive_path.read_bytes()).hexdigest(),
    "scope": "Gerbonara 1.6.3 syntax parsing, four copper layers, and full plated/nonplated drill geometry with 0.0002 mm coordinate tolerance. Gerbonara does not interpret the Gerber LR aperture-rotation commands in paste layers; their diagnostics are retained. This is not a fabrication or impedance certification.",
}
Path("routing/gerber-check.json").write_text(json.dumps(report, indent=2) + "\n")
print(json.dumps({"passed": report["passed"], "checks": checks, "parserDiagnosticCount": len(diagnostics)}, indent=2))
if not report["passed"]:
    sys.exit(1)