shibosoftwaredev/f1c100s-linux-nema17-stepper-controller

A four-layer NEMA 17 motor-controller PCB combining an Allwinner F1C100S Linux computer, USB‑PD/buck power system, W5500 Ethernet, STM32 real-time motion control, DRV8825 stepper driver, protection/temperature/current telemetry, USB interfaces, and local LED/buzzer diagnostics.

Version
0.4.56
License
unset
Stars
0

scripts/prepare-bulk-top-closure.py

"""Connect actual copper islands reported by the independent source-path audit.

Every virtual start is an existing through-via in the start pad's physical
component, never an assumed net connection. The resulting copper is audited
and reimported against every original source connection before installation.
"""
from route_geometry import *
from shapely.strtree import STRtree
from collections import defaultdict
c=load_circuit('routing/review-placement.circuit.json');root=connectivity(c)
m=json.loads(Path('routing/net-map.json').read_text())['roots']
sources={e['source_trace_id']:e for e in c if e['type']=='source_trace'}
ports={e['pcb_port_id']:e for e in c if e['type']=='pcb_port'}
def sn(s):return m[root((s.get('connected_source_port_ids',[])+s.get('connected_source_net_ids',[]))[0])]
net_sources=defaultdict(list)
for s in sources.values():net_sources[sn(s)].append(s)
snapshot=os.environ.get('CLOSURE_SNAPSHOT')
physical=json.loads(Path(snapshot or 'routing/bulk-top-physical-candidate.json').read_text())
patches=json.loads(Path(os.environ.get('CLOSURE_PATCHES','routing/bulk-top-island-patches.json')).read_text())
previous_removed=(json.loads(Path('reports/bulk-top-physical-islands.json').read_text())['removedForManualEscapes'] if snapshot else [])
for replacement in patches.get('replaceWires',[]):
    matches=[w for w in physical['wires'] if w['net']==replacement['net'] and w['layer']==replacement['layer']
             and len(w['points'])==2 and all(any(math.dist(a,b)<1e-7 for b in replacement['points']) for a in w['points'])]
    assert len(matches)==1,('Resolve exact old segment before reshaping it',replacement,matches)
    physical['wires']=[w for w in physical['wires'] if w is not matches[0]]
for v in patches.get('removeVias',[]):
    matches=[p for p in physical['vias'] if p['net']==v['net'] and math.dist((v['x'],v['y']),(p['x'],p['y']))<1e-7]
    assert len(matches)==1,('Resolve exact old via before relocating it',v,matches)
    physical['vias']=[p for p in physical['vias'] if p is not matches[0]]
patch_shapes=[(w['net'],[w['layer']],LineString(w['points']).buffer(w['width']/2)) for w in patches['wires']]
patch_shapes += [(v['net'],LAYERS,Point(v['x'],v['y']).buffer(.225)) for v in patches['vias']]
# Final placement also moves one small link onto the clear top-side strip.
# Rip only colliding low-current wire segments, never an existing power trunk.
names={e['source_component_id']:e['name'] for e in c if e['type']=='source_component'}
placed={e['pcb_component_id']:names[e['source_component_id']] for e in c if e['type']=='pcb_component'}
for e in c:
    if e['type']=='pcb_smtpad' and placed.get(e['pcb_component_id']) in json.loads(Path('review-placement.json').read_text()):
        p=ports[e['pcb_port_id']]
        patch_shapes.append((m[root(p['source_port_id'])],[e['layer']],geometry(e)))
removed=[];kept=[]
for w in physical['wires']:
    g=LineString(w['points']).buffer(w['width']/2)
    clash=any(n!=w['net'] and w['layer'] in ls and g.distance(h)<.1001 for n,ls,h in patch_shapes)
    clash|=any(v['net']!=w['net'] and g.distance(Point(v['x'],v['y']))<.354 for v in patches['vias'])
    if clash:
        assert w['width']<.299,('Manual signal escape must not cut an existing power trunk',w)
        removed.append(w)
    else:kept.append(w)
physical['wires']=kept+patches['wires']
for v in patches['vias']:
    assert all(math.dist((v['x'],v['y']),(p['x'],p['y']))>=.554 for p in physical['vias']),('Stitch spacing',v)
physical['vias']+=patches['vias']
Path('routing/bulk-top-closure-seed-physical.json').write_text(json.dumps(physical,indent=2)+'\n')
objects=defaultdict(list);retained=[]
for i,w in enumerate(physical['wires']):
    sid=net_sources[w['net']][0]['source_trace_id']
    retained.append(dict(type='pcb_trace',pcb_trace_id=f'closure_retained_wire_{i}',source_trace_id=sid,
        route=[dict(route_type='wire',x=x,y=y,layer=w['layer'],width=w['width']) for x,y in w['points']]))
    objects[w['net']].append(dict(shape=LineString(w['points']).buffer(w['width']/2,quad_segs=16),layers=[w['layer']]))
for i,v in enumerate(physical['vias']):
    sid=net_sources[v['net']][0]['source_trace_id']
    retained.append(dict(type='pcb_trace',pcb_trace_id=f'closure_retained_via_{i}',source_trace_id=sid,
        route=[dict(route_type='via',x=v['x'],y=v['y'],from_layer='top',to_layer='bottom',via_diameter=.45,via_hole_diameter=.3)]))
    objects[v['net']].append(dict(shape=Point(v['x'],v['y']).buffer(.225,quad_segs=16),layers=LAYERS,via=v))
for e in c:
    if e['type'] in ['pcb_smtpad','pcb_plated_hole'] and e.get('pcb_port_id') in ports:
        p=ports[e['pcb_port_id']];n=m[root(p['source_port_id'])]
        objects[n].append(dict(shape=geometry(e,'outer_' if e['type']=='pcb_plated_hole' else ''),
            layers=e.get('layers',[e.get('layer')]),port=p))
jobs=[];islands=[]
for n in sorted(net_sources):
    own=objects[n];tree=STRtree([o['shape'] for o in own]);remaining=set(range(len(own)));components=[]
    while remaining:
        todo=[next(iter(remaining))];seen=set()
        while todo:
            i=todo.pop()
            if i in seen:continue
            seen.add(i);remaining.discard(i);a=own[i]
            for j in tree.query(a['shape'].buffer(1e-7)):
                j=int(j);b=own[j]
                if j not in seen and set(a['layers'])&set(b['layers']) and a['shape'].distance(b['shape'])<1e-7:todo.append(j)
        pp={own[i]['port']['pcb_port_id']:own[i]['port'] for i in seen if 'port' in own[i]}
        if pp:components.append(dict(indices=seen,ports=list(pp.values())))
    components.sort(key=lambda comp:(-len(comp['ports']),-len(comp['indices'])))
    if len(components)<=1:continue
    main=components[0];anchor=main['ports'][0]
    for k,comp in enumerate(components[1:]):
        p=min(comp['ports'],key=lambda p:math.dist((p['x'],p['y']),(anchor['x'],anchor['y'])))
        choices=[s for s in net_sources[n] if p['source_port_id'] in s.get('connected_source_port_ids',[])]
        assert choices,('No source for island terminal',n,p)
        s=choices[0];vv=[own[i]['via'] for i in comp['indices'] if 'via' in own[i]]
        if vv:
            v=min(vv,key=lambda v:math.dist((v['x'],v['y']),(anchor['x'],anchor['y'])))
            start=dict(x=v['x'],y=v['y'],layers=LAYERS,pcb_port_id=f'actual_island_{n}_{k}_via')
        else:start={key:p[key] for key in ['x','y','layers','pcb_port_id']}
        width=max(s.get('min_trace_thickness',.12),{'N148':.3,'N149':.25,'N118':.25,'N125':.25,'N150':.3}.get(n,.15))
        target={**{key:anchor[key] for key in ['x','y','layers','pcb_port_id']},'connected_tree':n!='N148'}
        if n=='N148' and not os.environ.get('FOCUSED_REVIEW'):
            # The authored 0.30 mm fanout connects this through-via to
            # the real supply anchor. Do not land on a thinner old twig.
            assert any('via' in own[i] and math.dist((own[i]['via']['x'],own[i]['via']['y']),(11.32,-13.25))<1e-7 for i in main['indices'])
            target.update(x=11.32,y=-13.25,layers=LAYERS,pcb_port_id='retained_n148_anchor_stitch',actual_pad_port=anchor['pcb_port_id'])
        if n=='N160' and not os.environ.get('FOCUSED_REVIEW'):
            # Route toward the larger existing status-side copper tree.
            # This start is a real stitch in the MCU/debug component.
            vvmain=[own[i]['via'] for i in main['indices'] if 'via' in own[i]]
            v=next(v for v in vvmain if math.dist((v['x'],v['y']),(13.45,-3.2))<1e-7)
            start=dict(x=v['x'],y=v['y'],layers=LAYERS,pcb_port_id='retained_mcu_status_stitch')
            landing=comp['ports'][0]
            target={**{key:landing[key] for key in ['x','y','layers','pcb_port_id']},'connected_tree':True}
        if n=='N162' and not os.environ.get('FOCUSED_REVIEW'):
            # Both endpoint fanouts are explicit, pad-safe conductors.
            # Start and land on their real through-stitches.
            for xy in [(19.75,-18.2),(12.664442,-.536658)]:
                assert any('via' in o and math.dist((o['via']['x'],o['via']['y']),xy)<1e-7 for o in own)
            start=dict(x=19.75,y=-18.2,layers=LAYERS,pcb_port_id='authored_temperature_clock_stitch')
            target=dict(x=12.664442,y=-.536658,layers=LAYERS,pcb_port_id='authored_mcu_clock_stitch',connected_tree=True)
        if n=='N152' and not os.environ.get('FOCUSED_REVIEW'):
            # Start at the existing Ethernet signal stitch rather than
            # searching outward from the small termination-pad pocket.
            old=(-7.196846,-12.067678)
            assert any('via' in own[i] and math.dist((own[i]['via']['x'],own[i]['via']['y']),old)<1e-7 for i in main['indices'])
            start=dict(x=old[0],y=old[1],layers=LAYERS,pcb_port_id='retained_ethernet_termination_stitch')
            target=dict(x=15.9,y=-5.95,layers=LAYERS,pcb_port_id='authored_ethernet_termination_stitch',connected_tree=True)
        jobs.append(dict(source=s['source_trace_id'],start=start,constraints=dict(min_trace_thickness=width),
            target=target))
    islands.append(dict(net=n,components=len(components),ports=[len(comp['ports']) for comp in components],
        details=[dict(ports=[dict(name=names[ next(e['source_component_id'] for e in c if e['type']=='source_port' and e['source_port_id']==p['source_port_id']) ],x=p['x'],y=p['y']) for p in comp['ports']],
          vias=[own[i]['via'] for i in comp['indices'] if 'via' in own[i]]) for comp in components]))
def write(p,v):Path(p).write_text(json.dumps(v,indent=2)+'\n')
clock_priority={'N57':-4,'N209':-3,'N166':-2,'N62':-1,'N162':0,'N161':1,'N157':2}
jobs.sort(key=lambda job:(-job['constraints']['min_trace_thickness'],clock_priority.get(sn(sources[job['source']]),3)))
write('routing/bulk-top-closure-retained.json',retained)
write('routing/bulk-top-closure-plan.json',dict(prefix='bulk-top-closure',placement='routing/review-placement.circuit.json',
    retained='routing/bulk-top-closure-retained.json',extent=[-20.9,-20.9,20.9,20.9],jobs=jobs))
write('reports/bulk-top-physical-islands.json',dict(islands=islands,jobs=jobs,removedForManualEscapes=previous_removed+removed,relocatedVias=patches.get('removeVias',[])))
print(json.dumps(dict(islands=islands,jobs=len(jobs))))