AnasSarkiz/usb-c-pd-brushed-motor

USB-C PD-powered motor controller with protected input, selectable 5/9/12 V buck supply, MCU qualification, PWM H-bridge drive, overvoltage sensing, regenerative energy-dump resistors, and safety interlocks.

Version
0.0.1-a48-cloud-pipeline9-unrouted-c41d79a-305b1e7a
License
unset
Stars
0

scripts/review-usb-c-pad-shifts.py

"""Independently review the user-authorized C165948 pad translations.

Compare against the archived official import, preserve shapes and electrical
identity, and measure all adjacent copper lands without reducing clearance.
The manufacturer's20V rating is a separate unresolved power qualification.
"""
import argparse
import hashlib
import json
import re
from decimal import Decimal
from pathlib import Path

from shapely.geometry import Polygon, box


def read_footprint(source):
    pads = {}
    outside = []
    for line in source.splitlines():
        match = re.search(r'<smtpad portHints=\{\["pin(\d+)"\]\}', line)
        if not match:
            outside.append(line)
            continue
        pin = int(match.group(1))
        if 'shape="polygon"' in line:
            points = [(Decimal(x), Decimal(y)) for x, y in re.findall(r'\{x: "([-.0-9]+)mm", y: "([-.0-9]+)mm"\}', line)]
            shape = Polygon([(float(x), float(y)) for x, y in points])
            protected = re.sub(r'x: "[-.0-9]+mm"', 'x: "POSITION"', line)
            xs = [point[0] for point in points]
        else:
            attributes = dict(re.findall(r'(pcbX|pcbY|width|height)="([-.0-9]+)mm"', line))
            x, y, width, height = (Decimal(attributes[key]) for key in ('pcbX', 'pcbY', 'width', 'height'))
            shape = box(float(x - width / 2), float(y - height / 2), float(x + width / 2), float(y + height / 2))
            protected = re.sub(r'pcbX="[-.0-9]+mm"', 'pcbX="POSITION"', line)
            xs = [x]
        if pin in pads or not shape.is_valid:
            raise ValueError('Duplicate pin or invalid copper geometry')
        pads[pin] = {'xs': xs, 'shape': shape, 'protected': protected}
    return pads, outside


def review(source_path):
    original_path = Path('evidence/usb-c-C165948-original-import-A44.tsx.txt')
    original_bytes = original_path.read_bytes()
    if hashlib.sha256(original_bytes).hexdigest() != '7d3f0afe8f5b014edcfb3716ccf542929ae9d45550701aaa8bc63f2700c77bda':
        raise ValueError('Original official import evidence changed')
    original, original_outside = read_footprint(original_bytes.decode())
    current_bytes = source_path.read_bytes()
    current, current_outside = read_footprint(current_bytes.decode())
    if current_outside != original_outside or set(current) != set(original):
        raise ValueError('Identity, holes, CAD model or electrical pin inventory changed')
    shifts = []
    wide_nominals = {13: -3.2, 14: 3.2, 15: 2.4, 16: -2.4}
    for pin, pad in current.items():
        baseline = original[pin]
        if pad['protected'] != baseline['protected'] or len(pad['xs']) != len(baseline['xs']):
            raise ValueError(f'pin{pin}: size, shape, Y coordinate or assignment changed')
        translations = {new - old for new, old in zip(pad['xs'], baseline['xs'])}
        if len(translations) != 1:
            raise ValueError(f'pin{pin}: polygon was distorted rather than translated')
        shift = translations.pop()
        nominal_x_mm = -1.75 + (pin - 5) * 0.5 if pin <= 12 else wide_nominals[pin]
        center_x_mm = (pad['shape'].bounds[0] + pad['shape'].bounds[2]) / 2
        if abs(shift) > Decimal('0.001') or abs(center_x_mm - nominal_x_mm) > 0.05:
            raise ValueError(f'pin{pin}: excessive movement or manufacturer-layout mismatch')
        shifts.append({'pin': pin, 'translation_x_mm': float(shift), 'nominal_center_error_mm': center_x_mm - nominal_x_mm})
    ordered = sorted(current, key=lambda pin: current[pin]['shape'].bounds[0])
    gaps = [{'first_pin': first, 'second_pin': second, 'clearance_mm': current[first]['shape'].distance(current[second]['shape'])} for first, second in zip(ordered, ordered[1:])]
    if any(gap['clearance_mm'] < 0.2 for gap in gaps):
        raise ValueError('An actual adjacent-pad gap remains below0.20mm')
    return {'source': str(source_path), 'source_sha256': hashlib.sha256(current_bytes).hexdigest(), 'original_sha256': hashlib.sha256(original_bytes).hexdigest(), 'pads': len(current), 'changes_are_translations_only': True, 'minimum_adjacent_clearance_mm': min(gap['clearance_mm'] for gap in gaps), 'manufacturer_layout_tolerance_mm': 0.05, 'shifts': shifts, 'adjacent_gaps': gaps, 'power_qualification': 'Blocked: manufacturer20V rating does not establish21V maximum PD operation'}


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--source', type=Path, default=Path('imports/TYPE_C_31_M_12.tsx'))
    parser.add_argument('--report', type=Path)
    options = parser.parse_args()
    report = review(options.source)
    if options.report:
        options.report.write_text(json.dumps(report, indent=2) + '\n')
    print(json.dumps(report))