AnasSarkiz/rp2040-usb-c-single-axis-dm542t-motion-controller--01a0e89c

A USB‑C RP2040 controller board with external QSPI flash, crystal clock, regulated 3.3/1.1/5 V power, boost-converted motor-driver supply, protected USB and limit inputs, status LEDs, reset/boot switches, and AO3400A MOSFET outputs for stepper-control signals.

Version
0.1.0-prototype.19
License
unset
Stars
0

scripts/check-silkscreen.py

"""Measure actual native Gerber legend strokes against exposed board pads.

Supports the exporter’s absolute 4.6 mm, positive-polarity, circular-aperture
line/flash legend output. Rejects other drawing commands rather than guessing.
Board coordinates: mm, X right, Y up. No edits to manufacturing output.
"""
import hashlib
import json
import re
from pathlib import Path
from shapely.geometry import LineString, Point, box
from shapely.ops import unary_union
from copper_geometry import geometry


def read_legend(path):
    gerber = path.read_text()
    assert '%FSLAX46Y46*%' in gerber and '%MOMM*%' in gerber
    apertures = {int(number): float(diameter) for number, diameter in re.findall(r'%ADD(\d+)C,([\d.]+)\*%', gerber)}
    commands = gerber.split('G04 aperture END LIST*', 1)[1].splitlines()
    groups, strokes, widths = [], [], []
    position = None
    diameter = None
    for command in commands:
        command = command.strip()
        if not command or command.startswith('G04') or command in ['M02*', '%LPD*%', '%TD*%']:
            continue
        aperture = re.fullmatch(r'D(\d+)\*', command)
        if aperture:
            if strokes:
                groups.append((unary_union(strokes), diameter))
                strokes = []
            diameter = apertures[int(aperture[1])]
            widths.append(diameter)
            continue
        drawing = re.fullmatch(r'X(-?\d+)Y(-?\d+)D0([123])\*', command)
        assert drawing, f'Unsupported legend command: {command}'
        assert diameter is not None
        endpoint = (int(drawing[1]) / 1e6, int(drawing[2]) / 1e6)
        if drawing[3] == '1':
            assert position is not None
            strokes.append(LineString([position, endpoint]).buffer(diameter / 2, quad_segs=32))
        elif drawing[3] == '3':
            strokes.append(Point(endpoint).buffer(diameter / 2, quad_segs=32))
        position = endpoint
    if strokes:
        groups.append((unary_union(strokes), diameter))
    return groups, widths


if __name__ == '__main__':
    project = json.loads(Path('project.json').read_text())
    folder = Path('fabrication') / (project['hardware_revision'] + '-' + project['source_version'].removeprefix('0.1.0-'))
    assert folder.exists(), f'No fabrication export exists for current source: {folder}'
    circuit_path = Path('dist/index/circuit.json')
    circuit = json.loads(circuit_path.read_text())
    pads = [pad for pad in circuit if pad['type'] in ['pcb_smtpad', 'pcb_plated_hole'] and not pad.get('is_covered_with_solder_mask')]
    report = {'circuit_sha256': hashlib.sha256(circuit_path.read_bytes()).hexdigest(), 'layers': {}}
    for layer, filename in [('top', 'F_SilkScreen'), ('bottom', 'B_SilkScreen')]:
        groups, widths = read_legend(folder / 'exported' / f'{filename}.gbr')
        violations = []
        for index, (ink, diameter) in enumerate(groups):
            for pad in pads:
                if layer not in pad.get('layers', [pad.get('layer')]):
                    continue
                exposed = geometry(pad).buffer(pad.get('soldermask_margin', 0))
                clearance = ink.distance(exposed)
                if clearance < .15 - 1e-6:
                    violations.append({'ink_group': index, 'ink_bounds_mm': list(ink.bounds), 'pad': pad.get(pad['type'] + '_id'), 'clearance_mm': clearance})
            if not box(-44.8, -29.8, 44.8, 29.8).covers(ink):
                violations.append({'ink_group': index, 'ink_bounds_mm': list(ink.bounds), 'issue': 'ink_outside_outline_margin'})
        for index, (ink, diameter) in enumerate(groups):
            if abs(diameter - .162) >= 1e-6:
                continue
            for other_index, (other_ink, other_diameter) in enumerate(groups[index + 1:], index + 1):
                if abs(other_diameter - .162) < 1e-6 and ink.distance(other_ink) < .15 - 1e-6:
                    violations.append({'ink_group': index, 'other_ink_group': other_index, 'issue': 'printed_text_too_close'})
        text_heights = [ink.bounds[3] - ink.bounds[1] for ink, diameter in groups if abs(diameter - .162) < 1e-6]
        expected_text_count = sum(element['type'] == 'pcb_silkscreen_text' and element.get('layer') == layer for element in circuit)
        assert len(text_heights) == expected_text_count, 'Legend text count or font sizing changed; review required'
        report['layers'][layer] = {'minimum_stroke_mm': min(widths) if widths else None, 'minimum_text_height_mm': min(text_heights) if text_heights else None, 'violations': violations}
    (folder / 'silkscreen-review.json').write_text(json.dumps(report, indent=2) + '\n')
    for layer, result in report['layers'].items():
        print(layer, 'min stroke', result['minimum_stroke_mm'], 'violations', len(result['violations']))
    assert all(result['minimum_stroke_mm'] is None or result['minimum_stroke_mm'] >= .15 - 1e-6 for result in report['layers'].values())
    assert all(not result['violations'] for result in report['layers'].values()), 'See silkscreen-review.json'

    assert all(result['minimum_text_height_mm'] is None or result['minimum_text_height_mm'] >= 1 for result in report['layers'].values())