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)