seveibar/nema17-rp2040-stepper-controller

A tscircuit package created by seveibar.

Version
0.3.1
License
unset
Stars
0

scripts/audit-exported-clearances.py

"""Check NPTH clearances in the delivered Gerbers and Excellon, not just JSON."""
import math
import hashlib
import json
import re
import zipfile
from pathlib import Path
from shapely.geometry import Point, Polygon
from gerber_geometry import gerber_geometry

root = Path(__file__).resolve().parents[1]
circuit_file = root / 'dist/index/circuit.json'
archive = root / 'dist/index/gerbers.zip'
circuit = json.loads(circuit_file.read_text())
rules = json.loads((root / 'engineering/jlcpcb-clearance-rules.json').read_text())['minimumMm']
npths = [e for e in circuit if e['type'] == 'pcb_hole']
board = next(e for e in circuit if e['type'] == 'pcb_board')
outline = Polygon([(p['x'], p['y']) for p in board['outline']])
checks = []
with zipfile.ZipFile(archive) as z:
    drill = z.read('drill_npth.drl').decode()
    assert re.search(r'^METRIC$', drill, re.M) and 'G90' in drill
    tools = {int(n): float(d) for n, d in re.findall(r'^T(\d+)C([\d.]+)', drill, re.M)}
    selected = None; actual = []
    for line in drill.splitlines():
        if match := re.fullmatch(r'T(\d+)', line):
            selected = tools[int(match[1])]
        elif match := re.fullmatch(r'X(-?[\d.]+)Y(-?[\d.]+)', line):
            assert selected is not None
            actual.append((float(match[1]), float(match[2]), selected))
    expected = [(round(e['x'], 4), round(e['y'], 4), round(e['hole_diameter'], 6)) for e in npths]
    assert sorted(actual) == sorted(expected), 'Delivered NPTH drill positions/sizes differ from PCB'
    # Verify the eight connector retaining slots survived the Gerber/Excellon
    # exporter with supplier widths, lengths, positions, and orientation.
    plated = z.read('drill-L1-L4.drl').decode()
    ptools = {int(n): float(d) for n,d in re.findall(r'^T(\d+)C([\d.]+)', plated, re.M)}
    selected = None; start = None; slots = []
    for line in plated.splitlines():
        if match := re.fullmatch(r'T(\d+)', line):selected = ptools[int(match[1])]
        elif match := re.fullmatch(r'X(-?[\d.]+)Y(-?[\d.]+)', line):start = (float(match[1]),float(match[2]))
        elif match := re.fullmatch(r'G85X(-?[\d.]+)Y(-?[\d.]+)', line):
            assert start is not None and selected is not None
            end = (float(match[1]),float(match[2]))
            slots.append(((start[0]+end[0])/2,(start[1]+end[1])/2,
                          selected,math.dist(start,end)+selected))
    usb_sources = {e['source_component_id'] for e in circuit if e['type']=='source_component' and e['name'] in ['J_LAPTOP','J_MOTOR_USB']}
    usb_pcbs = {e['pcb_component_id'] for e in circuit if e['type']=='pcb_component' and e['source_component_id'] in usb_sources}
    expected_slots = [e for e in circuit if e['type']=='pcb_plated_hole' and e.get('pcb_component_id') in usb_pcbs]
    assert len(slots)==len(expected_slots)==8
    for e in expected_slots:
        assert e['shape']=='pill' and e.get('ccw_rotation',0)%180==0
        # This exporter rounds G85 endpoints to 0.001 mm (starts: 0.0001 mm).
        match = [a for a in slots if math.dist(a[:2],(e['x'],e['y']))<.0005]
        assert len(match)==1
        assert abs(match[0][2]-e['hole_width'])<.00001 and abs(match[0][3]-e['hole_height'])<.0006

    for layer, filename in [('top', 'F_Cu.gbr'), ('inner1', 'In1_Cu.gbr'),
                            ('inner2', 'In2_Cu.gbr'), ('bottom', 'B_Cu.gbr')]:
        copper = gerber_geometry(z.read(filename).decode())
        for e in npths:
            hole = Point(e['x'], e['y']).buffer(e['hole_diameter'] / 2, quad_segs=64)
            gap = copper.distance(hole)
            checks.append({'hole': e['pcb_hole_id'], 'layer': layer,
                           'distanceMm': gap, 'requiredMm': rules['npthToCopper']})
        edge_gap = copper.distance(outline.boundary)
        assert outline.covers(copper) and edge_gap >= rules['copperToBoardEdge'] - 1e-6
        checks.append({'boardEdge': True, 'layer': layer, 'distanceMm': edge_gap,
                       'requiredMm': rules['copperToBoardEdge']})
violations = [c for c in checks if c['distanceMm'] < c['requiredMm'] - 1e-6]
report = {'circuitJsonSha256': hashlib.sha256(circuit_file.read_bytes()).hexdigest(),
          'gerbersSha256': hashlib.sha256(archive.read_bytes()).hexdigest(),
          'npthDrillsVerified': len(actual), 'usbPlatedSlotsVerified': len(expected_slots), 'checks': checks, 'violations': violations}
(root / 'reports/exported-clearances.json').write_text(json.dumps(report, indent=2) + '\n')
print(json.dumps(report, indent=2))
assert not violations, 'Exported copper fails clearance checks'
, drill, re.M) and 'G90' in drill\n tools = {int(n): float(d) for n, d in re.findall(r'^T(\\d+)C([\\d.]+)', drill, re.M)}\n selected = None; actual = []\n for line in drill.splitlines():\n if match := re.fullmatch(r'T(\\d+)', line):\n selected = tools[int(match[1])]\n elif match := re.fullmatch(r'X(-?[\\d.]+)Y(-?[\\d.]+)', line):\n assert selected is not None\n actual.append((float(match[1]), float(match[2]), selected))\n expected = [(round(e['x'], 4), round(e['y'], 4), round(e['hole_diameter'], 6)) for e in npths]\n assert sorted(actual) == sorted(expected), 'Delivered NPTH drill positions/sizes differ from PCB'\n # Verify the eight connector retaining slots survived the Gerber/Excellon\n # exporter with supplier widths, lengths, positions, and orientation.\n plated = z.read('drill-L1-L4.drl').decode()\n ptools = {int(n): float(d) for n,d in re.findall(r'^T(\\d+)C([\\d.]+)', plated, re.M)}\n selected = None; start = None; slots = []\n for line in plated.splitlines():\n if match := re.fullmatch(r'T(\\d+)', line):selected = ptools[int(match[1])]\n elif match := re.fullmatch(r'X(-?[\\d.]+)Y(-?[\\d.]+)', line):start = (float(match[1]),float(match[2]))\n elif match := re.fullmatch(r'G85X(-?[\\d.]+)Y(-?[\\d.]+)', line):\n assert start is not None and selected is not None\n end = (float(match[1]),float(match[2]))\n slots.append(((start[0]+end[0])/2,(start[1]+end[1])/2,\n selected,math.dist(start,end)+selected))\n usb_sources = {e['source_component_id'] for e in circuit if e['type']=='source_component' and e['name'] in ['J_LAPTOP','J_MOTOR_USB']}\n usb_pcbs = {e['pcb_component_id'] for e in circuit if e['type']=='pcb_component' and e['source_component_id'] in usb_sources}\n expected_slots = [e for e in circuit if e['type']=='pcb_plated_hole' and e.get('pcb_component_id') in usb_pcbs]\n assert len(slots)==len(expected_slots)==8\n for e in expected_slots:\n assert e['shape']=='pill' and e.get('ccw_rotation',0)%180==0\n # This exporter rounds G85 endpoints to 0.001 mm (starts: 0.0001 mm).\n match = [a for a in slots if math.dist(a[:2],(e['x'],e['y']))\u003c.0005]\n assert len(match)==1\n assert abs(match[0][2]-e['hole_width'])\u003c.00001 and abs(match[0][3]-e['hole_height'])\u003c.0006\n\n for layer, filename in [('top', 'F_Cu.gbr'), ('inner1', 'In1_Cu.gbr'),\n ('inner2', 'In2_Cu.gbr'), ('bottom', 'B_Cu.gbr')]:\n copper = gerber_geometry(z.read(filename).decode())\n for e in npths:\n hole = Point(e['x'], e['y']).buffer(e['hole_diameter'] / 2, quad_segs=64)\n gap = copper.distance(hole)\n checks.append({'hole': e['pcb_hole_id'], 'layer': layer,\n 'distanceMm': gap, 'requiredMm': rules['npthToCopper']})\n edge_gap = copper.distance(outline.boundary)\n assert outline.covers(copper) and edge_gap \u003e= rules['copperToBoardEdge'] - 1e-6\n checks.append({'boardEdge': True, 'layer': layer, 'distanceMm': edge_gap,\n 'requiredMm': rules['copperToBoardEdge']})\nviolations = [c for c in checks if c['distanceMm'] \u003c c['requiredMm'] - 1e-6]\nreport = {'circuitJsonSha256': hashlib.sha256(circuit_file.read_bytes()).hexdigest(),\n 'gerbersSha256': hashlib.sha256(archive.read_bytes()).hexdigest(),\n 'npthDrillsVerified': len(actual), 'usbPlatedSlotsVerified': len(expected_slots), 'checks': checks, 'violations': violations}\n(root / 'reports/exported-clearances.json').write_text(json.dumps(report, indent=2) + '\\n')\nprint(json.dumps(report, indent=2))\nassert not violations, 'Exported copper fails clearance checks'\n","content_mimetype":"application/octet-stream","created_at":"2026-10-30T05:46:58.707Z"}; window.SSR_PACKAGE_BUILDS = []; window.SSR_RELATED_PACKAGES = [{"package_id":"b956dcd0-e37b-4022-a655-c93ee2f70790","creator_account_id":"9f398687-69d0-4ef4-a48c-bb6dc990e7df","latest_package_release_id":"05cd794e-886e-4afb-bce4-2057190a6e7b","last_built_release_id":"05cd794e-886e-4afb-bce4-2057190a6e7b","latest_package_release_fs_sha":"md5-02368e63f555bf5f853432c70765cb04","latest_pcb_preview_image_url":"https://api.tscircuit.com/package_files/view?package_release_id=05cd794e-886e-4afb-bce4-2057190a6e7b\u0026file_path=dist%2Findex%2Fpcb.svg","latest_sch_preview_image_url":"https://api.tscircuit.com/package_files/view?package_release_id=05cd794e-886e-4afb-bce4-2057190a6e7b\u0026file_path=dist%2Findex%2Fschematic.svg","latest_cad_preview_image_url":"https://api.tscircuit.com/package_files/view?package_release_id=05cd794e-886e-4afb-bce4-2057190a6e7b\u0026file_path=dist%2Findex%2F3d.png","latest_version":"0.1.5","license":"unset","owner_org_id":"f154d4e0-ac63-47a3-948e-a13da34df910","org_owner_tscircuit_handle":"seveibar","owner_github_username":"seveibar","github_repo_full_name":null,"is_source_from_github":false,"description":null,"name":"seveibar/am3352-dev-board-4layer-dogbone","unscoped_name":"am3352-dev-board-4layer-dogbone","star_count":0,"created_at":"2026-09-19T20:54:27.568Z","updated_at":"2026-09-21T23:56:14.470Z","ai_description":"Defines PCB footprints and pin mappings for a TI AM3352ZCZ100 processor, JST GH connectors, and assembly-part lookup helpers for capacitors, resistors, and headers.","ai_usage_instructions":null,"is_private":false,"is_unlisted":false,"is_public":true,"public_dist_enabled":false,"has_public_dist":true,"starred_at":null,"default_view":"files","website":null,"github_installation_id":null,"allow_pr_previews":false,"related_type":"same_author","thumbnail_url":"https://api.tscircuit.com/package_files/view?package_release_id=05cd794e-886e-4afb-bce4-2057190a6e7b\u0026file_path=dist%2Findex%2Fpcb.svg"},{"package_id":"37d3dd71-d7bb-4e34-8473-4e7de953f753","creator_account_id":"d8c33e0e-c5b9-42bf-a973-49b9132e4d39","latest_package_release_id":"77afff2e-4287-4faa-91c2-347ac418458b","last_built_release_id":"77afff2e-4287-4faa-91c2-347ac418458b","latest_package_release_fs_sha":"md5-ea65486bc390a58bf32350323fc6942d","latest_pcb_preview_image_url":"https://api.tscircuit.com/package_files/view?package_release_id=77afff2e-4287-4faa-91c2-347ac418458b\u0026file_path=dist%2Fassembly-reviewed%2Fpcb.svg","latest_sch_preview_image_url":"https://api.tscircuit.com/package_files/view?package_release_id=77afff2e-4287-4faa-91c2-347ac418458b\u0026file_path=dist%2Fassembly-reviewed%2Fschematic.svg","latest_cad_preview_image_url":"https://api.tscircuit.com/package_files/view?package_release_id=77afff2e-4287-4faa-91c2-347ac418458b\u0026file_path=dist%2Fcontroller%2F3d.png","latest_version":"0.1.0","license":"unset","owner_org_id":"a6bd85cf-cefd-4cb9-874e-ed21daca3f5d","org_owner_tscircuit_handle":"mohan-bee","owner_github_username":"mohan-bee","github_repo_full_name":null,"is_source_from_github":false,"description":null,"name":"mohan-bee/gimbal-one-budget-pcb","unscoped_name":"gimbal-one-budget-pcb","star_count":0,"created_at":"2026-10-22T07:24:04.021Z","updated_at":"2026-10-22T07:33:40.220Z","ai_description":"A 4-layer STM32-based gimbal controller PCB with an onboard LSM6DS3TR-C IMU, three DRV8313 3-phase motor drivers, protected 3S battery input, 3.3 V buck regulation, and SWD/UART control interfaces.","ai_usage_instructions":null,"is_private":false,"is_unlisted":false,"is_public":true,"public_dist_enabled":false,"has_public_dist":true,"starred_at":null,"default_view":"files","website":null,"github_installation_id":null,"allow_pr_previews":false,"related_type":"random","thumbnail_url":"https://api.tscircuit.com/package_files/view?package_release_id=77afff2e-4287-4faa-91c2-347ac418458b\u0026file_path=dist%2Fassembly-reviewed%2Fpcb.svg"}]; window.SSR_PACKAGE_ROUTE = {"author":"seveibar","packageName":"nema17-rp2040-stepper-controller","packageNameWithScope":"seveibar/nema17-rp2040-stepper-controller","version":null,"isLegacyRoute":false,"kind":"file","filePath":"scripts/audit-exported-clearances.py"};