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/route-round7-usb.py
"""Bounded local copper closure for the explicitly placed USB gate.
This does not modify the replay used by a normal build. Old non-USB copper
is fixed. Uses the exact new placement, 0.15 mm USB / 0.12 mm logic tracks,
0.30/0.45 mm through-vias, solder-land drill exclusions and original screw
envelopes. A separate import and complete emitted-copper validation follow.
"""
from route_geometry import *
from shapely.ops import unary_union,nearest_points
from shapely import contains_xy, prepare
from shapely.strtree import STRtree
from scipy.ndimage import distance_transform_edt
from collections import defaultdict
import numpy as np
import heapq,time
import os
rework=os.environ.get("ROUND8_REWORK") in ["1","motor","extended"]
motor_rework=os.environ.get("ROUND8_REWORK")=="motor"
extended_rework=os.environ.get("ROUND8_REWORK")=="extended"
round9_plan=json.loads(Path(os.environ['ROUND9_PLAN']).read_text()) if os.environ.get('ROUND9_PLAN') else None
c=load_circuit(round9_plan['placement'] if round9_plan else "routing/placement.json");root=connectivity(c)
mapping=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"}
by_source={p["source_port_id"]:p for p in ports.values()}
def net(t):
s=sources[t["source_trace_id"]]
return mapping[root((s.get("connected_source_port_ids",[])+s.get("connected_source_net_ids",[]))[0])]
retained=json.loads(Path(round9_plan['retained'] if round9_plan else "routing/round8-extended-retained.json" if extended_rework else "routing/round8-motor-retained.json" if motor_rework else "routing/round8-retained.json" if rework else "routing/round7-usb-retained.json").read_text())
objects=[];drills=[];pads=[];wires=[];seen=set();connected_ports=set()
def add_wire(n,l,a,b,w):
if math.dist(a,b)<1e-8:return
key=n,l,tuple(sorted([tuple(a),tuple(b)])),w
if key in seen:return
seen.add(key);line=LineString([a,b]);g=line.buffer(w/2,quad_segs=16)
objects.append(dict(net=n,layers=[l],shape=g,centerline=line,kind="wire"));wires.append(dict(net=n,layer=l,width=w,points=[a,b]))
def add_via(n,x,y):
if any(math.dist((x,y),(v["x"],v["y"]))<1e-6 for v in drills):return
drills.append(dict(net=n,x=x,y=y));objects.append(dict(net=n,layers=LAYERS,shape=Point(x,y).buffer(.225,quad_segs=16),kind="via"))
for t in retained:
n=net(t)
for p in t["route"]:
for k in ["start_pcb_port_id","end_pcb_port_id"]:
q=ports.get(p.get(k))
if q and mapping[root(q["source_port_id"])]==n and math.hypot(q["x"]-p["x"],q["y"]-p["y"])<1e-5:
connected_ports.add(q["pcb_port_id"])
if p["route_type"]=="via":add_via(n,p["x"],p["y"])
for a,b in zip(t["route"],t["route"][1:]):
if a["route_type"]==b["route_type"]=="wire" and a["layer"]==b["layer"]:
add_wire(n,a["layer"],(a["x"],a["y"]),(b["x"],b["y"]),max(a["width"],b["width"]))
native=json.loads(Path("routing/native-features.json").read_text())
for v in native["thermal_vias"]+native.get("auxiliary_vias",[]):add_via(v["net"],v["x"],v["y"])
holes=[];pth=[]
for e in c:
if e["type"] in ["pcb_smtpad","pcb_plated_hole"]:
p=ports.get(e.get("pcb_port_id"));n=mapping[root(p["source_port_id"])] if p else "NC:"+e[e["type"]+"_id"]
g=geometry(e,"outer_" if e["type"]=="pcb_plated_hole" else "")
item=dict(net=n,layers=e.get("layers",[e.get("layer")]),shape=g,kind="pad",port=e.get("pcb_port_id"))
objects.append(item);pads.append(item)
if e["type"]=="pcb_plated_hole":pth.append((n,geometry(e,"hole_")))
elif e["type"]=="pcb_hole":holes.append(geometry(e,"hole_"))
pitch=.05
extent=round9_plan['extent'] if round9_plan else (-20.9,-20.9,20.9,20.9) if extended_rework else (-11,-20.9,8,-8.5) if motor_rework else (-8,-2,12,11) if rework else (-20.9,7.5,20.9,20.9)
axisx=np.round(np.arange(extent[0],extent[2]+.0001,pitch),8);axisy=np.round(np.arange(extent[1],extent[3]+.0001,pitch),8)
xx,yy=np.meshgrid(axisx,axisy);ny,nx=xx.shape
region=box(axisx[0]-.5,axisy[0]-.5,axisx[-1]+.5,axisy[-1]+.5)
def raster(items):
g=unary_union(items)
prepare(g)
return contains_xy(g,xx,yy)
def coord(i,j):return float(axisx[i]),float(axisy[j])
def point_grid(x,y):return int(round((x-axisx[0])/pitch)),int(round((y-axisy[0])/pitch))
def route(s,start_override=None,target_override=None,force_inner=False):
n=net(s);ids=s.get("connected_source_port_ids",[])
pp=[by_source[p] for p in ids if p in by_source]
if len(pp)==2:
area=lambda p:sum(o["shape"].area for o in pads if o.get("port")==p["pcb_port_id"])
if pp[1]["pcb_component_id"]=="pcb_component_72" or pp[0]["pcb_component_id"]!="pcb_component_72" and area(pp[1])<area(pp[0]):pp.reverse()
if pp[0]["pcb_port_id"] in connected_ports and pp[1]["pcb_port_id"] not in connected_ports:pp.reverse()
cap_starts={"USB_GATE_SUPPLY":"U_USB_GATE","C_U_1V2_OUT_SUPPLY":"C_U_1V2_OUT","C_U_3V3_OUT_SUPPLY":"C_U_3V3_OUT"}
if rework:
moved=set(json.loads(Path("round8-placement.json").read_text()))
moved_ids={e["pcb_component_id"] for e in c if e["type"]=="pcb_component" and any(z["type"]=="source_component" and z["source_component_id"]==e["source_component_id"] and z["name"] in moved for z in c)}
pp.sort(key=lambda p:p["pcb_component_id"] not in moved_ids)
if s.get("name") in cap_starts:
cap_source=next(e["source_component_id"] for e in c if e["type"]=="source_component" and e["name"]==cap_starts[s["name"]])
cap_pcb=next(e["pcb_component_id"] for e in c if e["type"]=="pcb_component" and e["source_component_id"]==cap_source)
pp.sort(key=lambda p:p["pcb_component_id"]!=cap_pcb)
if not pp and not start_override:return dict(name=s.get("name"),skip="no physical terminals")
start=start_override or pp[0];start_xy=start["x"],start["y"];start_layers=start["layers"]
name=s.get("name","")
width=.3 if name=="U_USB_ESD_PIN2_CONNECTION" else .15 if name.startswith(("USB_DM","USB_DP","USB_GATE_HOST","USB_GATE_SOC")) or re.match(r"U_USB_ESD_PIN[1346]_",name) else .12
width=max(width,s.get("min_trace_thickness",0))
if motor_rework:width=max(width,.3)
nearby=[o for o in objects if o["shape"].intersects(region)]
blockers={l:[] for l in LAYERS};via_blocks=[];goals={l:[] for l in LAYERS};landings={l:[] for l in LAYERS}
target=target_override or (pp[1] if len(pp)>1 else None)
target_component=None
if target and (not target_override or target_override.get('connected_tree')):
# A real net tree may already reach this terminal through another
# source-associated path. Target that exact copper component, not
# an abstract net or an unrelated island. No synthesized link is used.
own=[o for o in objects if o["net"]==n]
tree=STRtree([o["shape"] for o in own])
todo=([i for i,o in enumerate(own) if set(o['layers'])&set(target['layers']) and o['shape'].buffer(1e-7).covers(Point(target['x'],target['y']))]
if target_override else [i for i,o in enumerate(own) if o.get("port")==target["pcb_port_id"]])
assert todo,('target must be actual same-net copper',target)
seen_own=set()
while todo:
i=todo.pop()
if i in seen_own:continue
seen_own.add(i);a=own[i]
for j in tree.query(a["shape"]):
j=int(j);b=own[j]
if j not in seen_own and set(a["layers"])&set(b["layers"]) and a["shape"].intersects(b["shape"]):todo.append(j)
target_component=[own[i] for i in seen_own]
if any(o.get("port")==start["pcb_port_id"] for o in target_component) or (start_override and any(set(o['layers'])&set(start_layers) and o['shape'].buffer(1e-7).covers(Point(start_xy)) for o in target_component)):
return dict(name=s.get("name"),source=s["source_trace_id"],alreadyConnectedByExplicitCopper=True)
for o in target_component:
for l in o["layers"]:
goals[l].append(o["shape"].buffer(width/2-.005));landings[l].append(o)
if target_override and not target_override.get('connected_tree'):
for l in target_override["layers"]:
g=Point(target_override["x"],target_override["y"])
goals[l].append(g.buffer(.03));landings[l].append(dict(kind="point",shape=g))
for o in nearby:
if o["net"]!=n:
for l in o["layers"]:blockers[l].append(o["shape"].buffer(.1003+width/2))
via_blocks.append(o["shape"].buffer(.354))
elif not target and (o["kind"]!="pad" or o.get("port") in connected_ports) and not o["shape"].covers(Point(start_xy)):
for l in o["layers"]:
goals[l].append(o["shape"].buffer(width/2-.005));landings[l].append(o)
if o["kind"]=="pad":via_blocks.append(o["shape"].buffer(.331))
if target and target_override is None and target_component is None and o.get("port")==target["pcb_port_id"]:
for l in o["layers"]:
goals[l].append(o["shape"].buffer(-.005));landings[l].append(o)
for v in drills:
p=Point(v["x"],v["y"]);via_blocks.append(p.buffer(.554))
if v["net"]!=n:
for l in LAYERS:blockers[l].append(p.buffer(.354+width/2))
for n2,h in pth:
via_blocks.append(h.buffer(.429))
if n2!=n:
for l in LAYERS:blockers[l].append(h.buffer((.3003 if l.startswith("inner") else .2803)+width/2))
for h in holes:
via_blocks.append(h.buffer(.429))
for l in LAYERS:blockers[l].append(h.buffer(.2003+width/2))
for x in [-15.5,15.5]:
for y in [-15.5,15.5]:
# Full-stack via annuli must clear the larger top screw head.
via_blocks.append(Point(x,y).buffer(3.1))
blockers["top"].append(Point(x,y).buffer(2.8503+width/2))
blockers["bottom"].append(Point(x,y).buffer(2.6003+width/2))
# Copper must leave the actual board-edge margin after accounting for
# half its width. The grid boundary alone is not a copper clearance.
edge=21.15-.2003-width/2
frame=region.difference(box(-edge,-edge,edge,edge))
for layer in LAYERS:blockers[layer].append(frame)
if motor_rework:
# These paths carry winding current, including the low-side current
# regulation shunts. Keep all replacements on outer copper.
blockers["inner1"].append(region);blockers["inner2"].append(region)
if name=="DRV_ISEN_B":
# Both independent shunt escapes selected the same legal stitch.
# Reserve that measured stitch for A, then give B another exit.
for layer in LAYERS:blockers[layer].append(Point(-7.3,-18.3).buffer(.354+width/2))
if s.get('allowed_layers'):
for layer in set(LAYERS)-set(s['allowed_layers']):blockers[layer].append(region)
if force_inner:
# Engineering constraint for the low-current 3.3 V crossing: leave
# the top-side USB escape channel open, using two legal through-vias.
blockers["top"].append(box(-10.9,7,-9.9,22))
blockers["bottom"].append(region)
if name=="USB_DATA_VBUS_JOIN":
# Do not bridge VBUS laterally across the SMT pin bank: that would
# fence off the CC and data-pin escapes. Join the VBUS ends using
# legal through-vias and an inner-layer crossing instead.
blockers["top"].append(box(-8.97,7,-5.03,22))
if start.get("pcb_port_id")=="ground_cap_junction":
# Keep the rebuilt ground bridge north of the moved capacitor,
# leaving the switch's short VCC/bypass escape corridor open.
blockers["bottom"].append(box(-22,7,22,12.75))
if name=="USB_ATTACH_OE":
blockers["bottom"].append(box(-22,7,22,11.45))
if name in ["USB_GATE_VCC","USB_GATE_SUPPLY"]:
# Supply and bypass leave north of the package. Keep the west
# middle-pin escape free for USB P instead of crossing it in copper.
blockers["bottom"].append(box(-22,7,22,11.55))
if name=="USB_GATE_GND":
# Leave south before turning west: the two west-side USB channels
# need the rows above this ground-return track for their escapes.
blockers["bottom"].append(box(-22,10.4,-10.1,22))
blockers["bottom"].append(box(-9.64,7,22,22))
if name=="USB_GATE_SOC_P":
# Reserve the lower-west through-via cell for the host-side P pin.
for layer in LAYERS:
blockers[layer].append(box(-11.45,10.3,-10.8,10.95))
blockers[layer].append(Point(-11.1,10.8).buffer(.5))
if name=="C_U_1V2_OUT_SUPPLY":
# Do not reclaim the host-VBUS escape pocket for the core rail.
for layer in LAYERS:blockers[layer].append(Point(-4.93,14.1272).buffer(.55))
if name in ["C_U_1V2_OUT_SUPPLY","C_U_3V3_OUT_SUPPLY"]:
# Regulator-capacitor connections use the east/north side. Keep
# the four USB data fanout cells to the west unoccupied by power.
for layer in LAYERS:blockers[layer].append(box(-12.5,9.5,-8.75,12.5))
blocked=[raster(blockers[l]) for l in LAYERS]
goal=[raster(goals[l]) for l in LAYERS]
via_ok=~raster(via_blocks)
via_ok&=(np.abs(xx)<20.72)&(np.abs(yy)<20.72)
if motor_rework and name=="DRV_ISEN_B":via_ok&=np.hypot(xx+7.3,yy+18.3)>.6
if name=="USB_GATE_GND":
# Put the ground transition south of the USB-P escape corridor.
# The closer west-side via otherwise fences that signal in.
via_ok&=(yy<=10.45)&(xx<=-11.5)
if name=="USB_GATE_SOC_P":via_ok&=(yy>=11.1)&(xx<=-11.55)
any_goal=np.logical_or.reduce(goal)
assert any_goal.any(),("no connected goal in USB region",s["source_trace_id"],s.get("name"))
assert any((goal[i]&~blocked[i]).any() for i in range(4)),("every goal blocked",s["source_trace_id"],s.get("name"))
distance=distance_transform_edt(~any_goal)
heap=[];costs={};previous={};prefix={};blocked_geometries=[unary_union(blockers[l]) for l in LAYERS]
ix,jy=point_grid(*start_xy)
for l in start_layers:
li=LAYERS.index(l)
for i in range(max(0,ix-2),min(nx,ix+3)):
for j in range(max(0,jy-2),min(ny,jy+3)):
p=coord(i,j)
if blocked[li][j,i] or LineString([start_xy,p]).intersects(blocked_geometries[li]):continue
node=li,i,j;cost=math.dist(start_xy,p)/pitch
if cost<costs.get(node,math.inf):costs[node]=cost;prefix[node]=[start_xy,p];heapq.heappush(heap,(cost+distance[j,i],cost,node))
if not heap:
print(json.dumps(dict(blockedTerminal=start_xy,foreign=[dict(net=o["net"],kind=o["kind"],bounds=o["shape"].bounds,distance=o["shape"].distance(Point(start_xy))) for o in nearby if o["net"]!=n and start_layers[0] in o["layers"] and o["shape"].distance(Point(start_xy))<.22])),flush=True)
assert heap,("no legal terminal escape",s["source_trace_id"],s.get("name"),start_xy)
def exact_landing(li,xy):
p=Point(xy);choices=[]
for o in landings[LAYERS[li]]:
if p.distance(o["shape"])>width/2+.015:continue
if o["kind"]=="wire":g=o["centerline"]
elif o["kind"]=="pad":
g=o["shape"].buffer(-width/2-.01)
if g.is_empty:continue
else:g=o["shape"].centroid
q=nearest_points(p,g)[1];end=(q.x,q.y);dist=p.distance(q)
# Snap endpoint landings to the actual existing vertex. At an
# interior landing cross the centerline, keeping the exact
# projection as a real intermediate copper vertex below.
anchor=end
if o["kind"]=="wire":
endpoints=list(g.coords)
vertex=min(endpoints,key=lambda v:math.dist(v,end))
if math.dist(vertex,end)<1e-7:
anchor=end=tuple(vertex)
elif dist>1e-8:
end=(q.x+(q.x-p.x)*.01/dist,q.y+(q.y-p.y)*.01/dist)
stub=LineString([xy,end])
if dist<1e-8 or not stub.intersects(blocked_geometries[li]):choices.append((dist,anchor,end))
return min(choices,key=lambda a:a[0])[1:] if choices else None
began=time.monotonic();iterations=0;found=None;landing=None
while heap:
_,cost,node=heapq.heappop(heap);li,i,j=node
if cost>costs[node]+1e-8:continue
iterations+=1
if goal[li][j,i]:
landing=exact_landing(li,coord(i,j))
if landing is not None:found=node;break
if iterations>600000 or time.monotonic()-began>22:break
for di,dj in [(-1,0),(1,0),(0,-1),(0,1),(-1,-1),(-1,1),(1,-1),(1,1)]:
ii,jj=i+di,j+dj
if not(0<=ii<nx and 0<=jj<ny) or blocked[li][jj,ii]:continue
if di and dj and (blocked[li][j,ii] or blocked[li][jj,i]):continue
nxt=li,ii,jj;new=cost+math.hypot(di,dj)
if new<costs.get(nxt,math.inf):costs[nxt]=new;previous[nxt]=node;heapq.heappush(heap,(new+distance[jj,ii],new,nxt))
if via_ok[j,i]:
for ll in range(4):
if ll==li or blocked[ll][j,i]:continue
nxt=ll,i,j;new=cost+(100 if width==.15 else 35)
if new<costs.get(nxt,math.inf):costs[nxt]=new;previous[nxt]=node;heapq.heappush(heap,(new+distance[j,i],new,nxt))
if found is None:
checks=[]
samples=[(.2,-15.6),(.2,-16.2),(.2,-17),(2.2,-15.6),(2.2,-16.2)] if motor_rework else [(-4.1,14.2),(-4.1,14.4),(-4,14.5)]
for x,y in samples:
i,j=point_grid(x,y)
if not(0<=i<nx and 0<=j<ny):continue
checks.append(dict(x=x,y=y,via=bool(via_ok[j,i]),blocked=[bool(m[j,i]) for m in blocked],visited=[(l,i,j) in costs for l in range(4)]))
print(json.dumps(dict(start=start_xy,net=n,pocketNodes=len(costs),layers=list(set(l for l,i,j in costs)),checks=checks)),flush=True)
assert found is not None,("no local closure",s["source_trace_id"],s.get("name"),iterations)
path=[found]
while path[-1] in previous:path.append(previous[path[-1]])
path.reverse();points=prefix[path[0]]
points=[(path[0][0],*p) for p in points]+[(l,*coord(i,j)) for l,i,j in path[1:]]
for vertex in landing:
if math.dist(points[-1][1:],vertex)>1e-8:points.append((found[0],*vertex))
simple=[points[0]]
for a,b,d in zip(points,points[1:],points[2:]):
if a[0]!=b[0] or b[0]!=d[0] or abs((b[1]-a[1])*(d[2]-b[2])-(b[2]-a[2])*(d[1]-b[1]))>1e-8:simple.append(b)
simple.append(points[-1])
for a,b in zip(simple,simple[1:]):
if a[0]!=b[0]:add_via(n,a[1],a[2])
else:add_wire(n,LAYERS[a[0]],a[1:],b[1:],width)
connected_ports.add(start["pcb_port_id"])
if target:connected_ports.add(target["pcb_port_id"])
return dict(name=s.get("name"),source=s["source_trace_id"],net=n,width=width,iterations=iterations,
seconds=round(time.monotonic()-began,2),points=simple)
def write_candidate(prefix,completed):
Path(f"routing/{prefix}-physical-candidate.json").write_text(json.dumps(dict(wires=wires,vias=drills),indent=2)+"\n")
net_ids=sorted({w["net"] for w in wires}|{v["net"] for v in drills})
num=lambda x:str(round(float(x)*10000,4))
rows=["(session local_rework (routes (resolution um 10) (network_out"]
for n in net_ids:
rows.append(f"(net {n}")
for w in wires:
if w["net"]==n:rows.append(f"(wire (path {w['layer']} {num(w['width'])} "+" ".join(num(v) for p in w["points"] for v in p)+"))")
for v in drills:
if v["net"]==n:rows.append(f"(via VIA {num(v['x'])} {num(v['y'])})")
rows.append(")")
rows.append(")))")
Path(f"routing/{prefix}-candidate.ses").write_text("\n".join(rows)+"\n")
Path(f"reports/{prefix}-closure.json").write_text(json.dumps(dict(complete=True,notInstalled=True,routes=completed),indent=2)+"\n")
print(json.dumps(dict(complete=True,wires=len(wires),vias=len(drills),notInstalled=True)),flush=True)
if round9_plan:
prefix=round9_plan.get('prefix','round9-coordinated')
completed=[]
try:
for job in round9_plan['jobs']:
s={**sources[job['source']],**job.get('constraints',{})}
result=route(s,start_override=job.get('start'),target_override=job.get('target'))
completed.append(result)
print(json.dumps({k:v for k,v in result.items() if k!='points'}),flush=True)
except Exception as exc:
write_candidate(prefix+'-partial',completed)
Path(f'reports/{prefix}-attempt.json').write_text(json.dumps(dict(complete=False,notInstalled=True,error=str(exc),routes=completed),indent=2)+'\n')
raise
write_candidate(prefix,completed)
raise SystemExit(0)
if rework:
affected=["source_trace_336","source_trace_334","source_trace_353","source_trace_354","source_trace_355","source_trace_356"] if motor_rework else json.loads(Path("reports/round8-extended-routing-plan.json" if extended_rework else "reports/round8-routing-plan.json").read_text())["affectedSourceTraceIds"]
if motor_rework and os.environ.get("MOTOR_FIRST")=="A":affected[:2]=list(reversed(affected[:2]))
completed=[]
try:
for sid in affected:
result=route(sources[sid]);completed.append(result)
print(json.dumps({k:v for k,v in result.items() if k!="points"}),flush=True)
except Exception as exc:
prefix="round8-extended" if extended_rework else "round8-motor" if motor_rework else "round8"
Path(f"reports/{prefix}-attempt.json").write_text(json.dumps(dict(complete=False,notInstalled=True,error=str(exc),routes=completed),indent=2)+"\n")
raise
write_candidate("round8-extended" if extended_rework else "round8-motor" if motor_rework else "round8",completed)
raise SystemExit(0)
affected=json.loads(Path("reports/round7-usb-routing-plan.json").read_text())["affectedSourceTraceIds"]
work=[sources[s] for s in affected]
priority={"C_U_1V2_OUT_SUPPLY":-2,"C_U_3V3_OUT_SUPPLY":-1,"USB_GATE_GND":0,"USB_GATE_VCC":1,"USB_GATE_SUPPLY":2,"USB_ATTACH_OE":3,"USB_GATE_SOC_N":4,"USB_GATE_SOC_P":5,"USB_GATE_HOST_P":6,"USB_GATE_HOST_N":7,"USB_DATA_VBUS_JOIN":8,"USB_DM_JOIN":9,"USB_DP_JOIN":9,"U_USB_ESD_PIN1_CONNECTION":10,"U_USB_ESD_PIN3_CONNECTION":10,"USB_DATA_CC2_RD":11}
work.sort(key=lambda s:(priority.get(s.get("name"),12 if len(s.get("connected_source_port_ids",[]))==2 else 13),s["source_trace_id"]))
completed=[]
try:
# Another source-associated ground tree duplicated the old ESD pad's
# top-side leaf. That pad moved to the bottom: retire its obsolete dead
# spur too, rather than treating it as a fixed obstacle or a terminal.
old_esd_ground=[(-12.3582,10.2639),(-11.6221,11),(-10.39,11)]
gnet=net(next(s for s in sources.values() if s.get("name")=="U_USB_ESD_PIN2_CONNECTION"))
for w in list(wires):
if w["net"]==gnet and w["layer"]=="top" and any(all(any(math.dist(q,p)<1e-6 for p in w["points"]) for q in [a,b]) for a,b in zip(old_esd_ground,old_esd_ground[1:])):
wires.remove(w);g=LineString(w["points"]).buffer(w["width"]/2,quad_segs=16)
objects[:]=[o for o in objects if not(o["kind"]=="wire" and o["net"]==gnet and o["layers"]==["top"] and o["shape"].equals(g))]
# Retire the moved CC2 resistor's old ground spur. Moving this small
# resistor opens the north-west control escape without changing the
# buzzer/MCU rail or its existing length budget.
old_cc_ground=[((-12.6,12.79),(-12.305,12.495)),
((-12.305,12.495),(-12.9,11.9)),
((-12.9,11.9),(-13.8,11.9)),
((-13.8,11.9),(-14.4,11.3)),
((-12.6,12.79),(-12.2,12.6))]
cc_ground=net(next(s for s in sources.values() if s.get("name")=="USB_DATA_CC2_GND"))
for w in list(wires):
if w["net"]==cc_ground and w["layer"]=="bottom" and any(all(any(math.dist(q,p)<1e-6 for p in w["points"]) for q in [a,b]) for a,b in old_cc_ground):
wires.remove(w);g=LineString(w["points"]).buffer(w["width"]/2,quad_segs=16)
objects[:]=[o for o in objects if not(o["kind"]=="wire" and o["net"]==cc_ground and o["layers"]==["bottom"] and o["shape"].equals(g))]
# Remove the former capacitor/ESD ground spur. Its solder-land contact
# endpoints moved; the retained branch at (-12.2,12.6) is reconnected by
# the new explicit component-to-ground routes, not an imaginary pad.
old_ground=[(-8.9575,11.4),(-9.5175,11.4),(-9.6575,11.4),(-9.75,11.3075),(-9.75,12.2),(-10.15,12.6),(-12.2,12.6)]
gn=net(next(s for s in sources.values() if s.get("name")=="C_U_3V3_OUT_GROUND"))
for w in list(wires):
if w["net"]==gn and w["layer"]=="bottom" and any(all(any(math.dist(q,p)<1e-6 for p in w["points"]) for q in [a,b]) for a,b in zip(old_ground,old_ground[1:])):
wires.remove(w);g=LineString(w["points"]).buffer(w["width"]/2,quad_segs=16)
objects[:]=[o for o in objects if not(o["kind"]=="wire" and o["net"]==gn and o["layers"]==["bottom"] and o["shape"].equals(g))]
# The former regulator-capacitor supply leaf has no terminal after the
# capacitor move. Retire both segments, including its wide contact stub.
old_cap_supply=[((-8.4923,9.3615),(-10.790192,11.391293)),
((-10.790192,11.391293),(-10.7825,11.4))]
n2=net(next(s for s in sources.values() if s.get("name")=="C_U_3V3_OUT_SUPPLY"))
for w in list(wires):
if w["net"]==n2 and w["layer"]=="bottom" and any(all(any(math.dist(q,p)<1e-6 for p in w["points"]) for q in [a,b]) for a,b in old_cap_supply):
wires.remove(w);g=LineString(w["points"]).buffer(w["width"]/2,quad_segs=16)
objects[:]=[o for o in objects if not(o["kind"]=="wire" and o["net"]==n2 and o["layers"]==["bottom"] and o["shape"].equals(g))]
# The former P-side ESD transition sits in the south-channel escape.
# Retire it and its obsolete inner stub; reconnect P to its retained
# inner2 trunk at a new legal full-through transition north of the gate.
pnet=net(next(s for s in sources.values() if s.get("name")=="USB_GATE_SOC_P"))
old_p=(-10.6536,8.8317)
drills[:]=[v for v in drills if not(v["net"]==pnet and math.dist((v["x"],v["y"]),old_p)<1e-6)]
old_shape=Point(old_p).buffer(.225,quad_segs=16)
objects[:]=[o for o in objects if not(o["kind"]=="via" and o["net"]==pnet and o["shape"].equals(old_shape))]
for w in list(wires):
if w["net"]==pnet and w["layer"]=="inner2" and all(any(math.dist(q,p)<1e-6 for p in w["points"]) for q in [old_p,(-8.6541,10.8312)]):
wires.remove(w);g=LineString(w["points"]).buffer(w["width"]/2,quad_segs=16)
objects[:]=[o for o in objects if not(o["kind"]=="wire" and o["net"]==pnet and o["layers"]==["inner2"] and o["shape"].equals(g))]
# Retire the old ESD-end data stubs instead of forcing the new switch
# to fit around them. Keep only the checked SoC-side fanout and vias.
old_data_chains={"USB_GATE_SOC_N":[(-8.41,11.5),(-8.41,11.8217),(-8.2495,11.9822),(-4.446,11.9822)],
"USB_GATE_SOC_P":[(-8.41,10.5),(-9.6075,10.5),(-10.6536,9.4539),(-10.6536,8.8317)]}
for name,chain in old_data_chains.items():
n=net(next(s for s in sources.values() if s.get("name")==name))
displaced=[w for w in wires if w["net"]==n and w["layer"]=="top" and any(all(any(math.dist(q,p)<1e-6 for p in w["points"]) for q in [a,b]) for a,b in zip(chain,chain[1:]))]
assert len(displaced)==3,("expected three old ESD-end data segments",name,displaced)
for w in displaced:
wires.remove(w);g=LineString(w["points"]).buffer(w["width"]/2,quad_segs=16)
objects[:]=[o for o in objects if not(o["kind"]=="wire" and o["net"]==w["net"] and o["layers"]==["top"] and o["shape"].equals(g))]
# Route the existing rail around the relocated output-capacitor land.
# Retain both real three-way junctions and the original 0.12 mm width.
rail_pass=next(s for s in sources.values() if s.get("name")=="USB_GATE_SUPPLY")
pass_chain=[(-7.5094,16.3906),(-7.714,16.5952),(-9.2623,16.5952),(-10.5335,15.324)]
displaced=[w for w in wires if w["net"]==net(rail_pass) and w["layer"]=="bottom" and any(all(any(math.dist(q,p)<1e-6 for p in w["points"]) for q in [a,b]) for a,b in zip(pass_chain,pass_chain[1:]))]
assert len(displaced)==3,("expected three cap-area rail segments",displaced)
for w in displaced:
wires.remove(w);g=LineString(w["points"]).buffer(w["width"]/2,quad_segs=16)
objects[:]=[o for o in objects if not(o["kind"]=="wire" and o["net"]==w["net"] and o["layers"]==["bottom"] and o["shape"].equals(g))]
result=route(rail_pass,dict(x=pass_chain[0][0],y=pass_chain[0][1],layers=["bottom"],pcb_port_id="cap_area_rail_a"),dict(x=pass_chain[-1][0],y=pass_chain[-1][1],layers=["bottom"],pcb_port_id="cap_area_rail_b"))
result["name"]="moved_3V3_cap_foreign_rail_detour";completed.append(result)
print(json.dumps({k:v for k,v in result.items() if k!="points"}),flush=True)
# The moved output capacitor shared a real bottom-side ground tree.
# Remove the three old branches which cross its new positive pad and
# rebuild all three junctions, rather than leaving stale copper behind.
ground=next(s for s in sources.values() if s.get("name")=="C_U_1V2_OUT_GROUND")
cap_supply=next(s for s in sources.values() if s.get("name")=="C_U_1V2_OUT_SUPPLY")
cap_chain=[(-8.82,14.755),(-8.82,14),(-5.488374,14.838374),(-4.25,15.15)]
cap_old=[w for w in wires if w["net"]==net(cap_supply) and w["layer"]=="bottom" and any(all(any(math.dist(q,p)<1e-6 for p in w["points"]) for q in [a,b]) for a,b in zip(cap_chain,cap_chain[1:]))]
for w in cap_old:
wires.remove(w);g=LineString(w["points"]).buffer(w["width"]/2,quad_segs=16)
objects[:]=[o for o in objects if not(o["kind"]=="wire" and o["net"]==w["net"] and o["layers"]==["bottom"] and o["shape"].equals(g))]
ground_chain=[((-5.7004,13.5),(-7.7783,13.5)),
((-7.7783,13.5),(-8.1733,13.105)),
((-8.1733,13.105),(-8.82,13.105)),
((-8.82,13.105),(-9.5175,13.105)),
((-9.5175,13.105),(-10.5858,13.105)),
((-9.5175,13.105),(-9.5175,11.4))]
displaced=[w for w in wires if w["net"]==net(ground) and w["layer"]=="bottom" and any(all(any(math.dist(q,p)<1e-6 for p in w["points"]) for q in [a,b]) for a,b in ground_chain)]
assert len(displaced)==6,("expected six moved-cap ground segments",displaced)
for w in displaced:
wires.remove(w);g=LineString(w["points"]).buffer(w["width"]/2,quad_segs=16)
objects[:]=[o for o in objects if not(o["kind"]=="wire" and o["net"]==w["net"] and o["layers"]==["bottom"] and o["shape"].equals(g))]
# The old third endpoint was a contact on the now-moved 3.3 V capacitor's
# ground land, not another terminal. Join the two retained real branches
# directly; each moved capacitor receives its own ground connection below.
for a in [(-5.7004,13.5)]:
result=route(ground,dict(x=a[0],y=a[1],layers=["bottom"],pcb_port_id="ground_cap_junction"),dict(x=-10.5858,y=13.105,layers=["bottom"],pcb_port_id="ground_existing_trunk"))
result["name"]="moved_cap_ground_tree";completed.append(result)
print(json.dumps({k:v for k,v in result.items() if k!="points"}),flush=True)
# Reclaim the old CORE-rail drill pocket for the previously unused host
# VBUS pad. Move that CORE transition near its regulator; keep the actual
# regulator-to-output-capacitor trunk and the SoC-side inner2 trunk.
old_core=(-4.93,14.1272);new_core=(-5.5,15.9)
rail=next(s for s in sources.values() if s.get("name")=="U_1V2_OUTPUT")
rail_net=net(rail)
removed_vias=[v for v in drills if math.dist((v["x"],v["y"]),old_core)<1e-6]
assert len(removed_vias)==1 and removed_vias[0]["net"]==rail_net
drills.remove(removed_vias[0])
old_shape=Point(old_core).buffer(.225,quad_segs=16)
objects[:]=[o for o in objects if not(o["kind"]=="via" and o["net"]==rail_net and o["shape"].equals(old_shape))]
chains={"bottom":[old_core,(-4.95,14.3),(-5.488374,14.838374)],
"inner2":[old_core,(-4.95,14.5174),(-4.0113,15.4561)]}
for layer,chain in chains.items():
displaced=[w for w in wires if w["net"]==rail_net and w["layer"]==layer and any(all(any(math.dist(q,p)<1e-6 for p in w["points"]) for q in [a,b]) for a,b in zip(chain,chain[1:]))]
assert len(displaced)==2,("expected two CORE transition segments",layer,displaced)
for w in displaced:
wires.remove(w);g=LineString(w["points"]).buffer(w["width"]/2,quad_segs=16)
objects[:]=[o for o in objects if not(o["kind"]=="wire" and o["net"]==rail_net and o["layers"]==[layer] and o["shape"].equals(g))]
q=Point(new_core)
assert all(o["net"]==rail_net or q.distance(o["shape"])>=.354 for o in objects),"new CORE drill clears all foreign copper"
assert all(q.distance(o["shape"])>=.331 for o in pads),"new CORE drill is outside every solder land"
assert all(math.dist(new_core,(v["x"],v["y"]))>=.554 for v in drills),"new CORE drill spacing"
assert all(q.distance(h)>=.429 for h in holes),"new CORE drill-to-NPTH clearance"
add_via(rail_net,*new_core)
for layer,end in [("bottom",(-4.25,15.15)),("inner2",(-4.0113,15.4561))]:
result=route(rail,dict(x=end[0],y=end[1],layers=[layer],pcb_port_id="core_trunk_junction"),dict(x=new_core[0],y=new_core[1],layers=[layer],pcb_port_id="new_core_transition"))
result["name"]="CORE_transition_"+layer;completed.append(result)
print(json.dumps({k:v for k,v in result.items() if k!="points"}),flush=True)
for s in work:
start=None
if s.get("name")=="USB_GATE_SUPPLY":
# Feed the already-connected bypass land, leaving OE's north
# escape free. The independent physical import still must prove
# the specified gate VCC terminal reaches this real copper tree.
source=next(e for e in c if e["type"]=="source_component" and e["name"]=="C_USB_GATE")
pin=next(e for e in c if e["type"]=="source_port" and e["source_component_id"]==source["source_component_id"] and e.get("pin_number")==1)
start=by_source[pin["source_port_id"]]
result=route(s,start_override=start);completed.append(result);print(json.dumps({k:v for k,v in result.items() if k!="points"}),flush=True)
# Restore the real ground-cluster bridge exposed by moving the old
# capacitor/CC ground lands. Both endpoints are existing ground copper.
ground_source=next(s for s in sources.values() if s.get("name")=="N_GND")
bridge={**ground_source,"name":"GND_CLUSTER_BRIDGE","min_trace_thickness":.3}
result=route(bridge,dict(x=.9,y=13.45,layers=["top"],pcb_port_id="avcc_existing_ground_via"),dict(x=.69,y=12.3,layers=["top"],pcb_port_id="soc_existing_ground_junction"))
completed.append(result);print(json.dumps({k:v for k,v in result.items() if k!="points"}),flush=True)
except Exception as exc:
Path("routing/round7-usb-physical-candidate.json").write_text(json.dumps(dict(wires=wires,vias=drills),indent=2)+"\n")
Path("reports/round7-usb-local-closure.json").write_text(json.dumps(dict(notInstalled=True,complete=False,error=str(exc),routes=completed),indent=2)+"\n")
raise
physical=dict(wires=wires,vias=drills)
Path("routing/round7-usb-physical-candidate.json").write_text(json.dumps(physical,indent=2)+"\n")
net_ids=sorted({w["net"] for w in wires}|{v["net"] for v in drills})
num=lambda x:str(round(float(x)*10000,4))
rows=["(session usb_local (routes (resolution um 10) (network_out"]
for n in net_ids:
rows.append(f"(net {n}")
for w in wires:
if w["net"]==n:rows.append(f"(wire (path {w['layer']} {num(w['width'])} "+" ".join(num(v) for p in w["points"] for v in p)+"))")
for v in drills:
if v["net"]==n:rows.append(f"(via VIA {num(v['x'])} {num(v['y'])})")
rows.append(")")
rows.append(")))")
Path("routing/round7-usb-candidate.ses").write_text("\n".join(rows)+"\n")
Path("reports/round7-usb-local-closure.json").write_text(json.dumps(dict(notInstalled=True,complete=True,routes=completed),indent=2)+"\n")
print(json.dumps(dict(complete=True,wires=len(wires),vias=len(drills),notInstalled=True)),flush=True)