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/check-round10-widths.py
"""Prove the repaired widths using actual emitted copper, never a plan file."""
from route_geometry import *
from shapely.ops import unary_union
from collections import defaultdict
import heapq,hashlib
path=Path('dist/index/circuit.json');c=load_circuit(str(path));root=connectivity(c)
sources={e['source_trace_id']:e for e in c if e['type']=='source_trace'}
by_name={e.get('name'):e for e in sources.values()}
def snet(s):return root((s.get('connected_source_port_ids',[])+s.get('connected_source_net_ids',[]))[0])
vm=snet(by_name['VM_PROTECTED'])
lines=defaultdict(list);all_widths=[]
for t in c:
if t['type']!='pcb_trace' or t.get('source_trace_id') not in sources or snet(sources[t['source_trace_id']])!=vm:continue
for a,b in zip(t['route'],t['route'][1:]):
if a['route_type']!=b['route_type'] or a['route_type']!='wire' or a['layer']!=b['layer']:continue
if math.dist((a['x'],a['y']),(b['x'],b['y']))<1e-8:continue
width=min(a['width'],b['width']);all_widths.append(width)
assert a['layer'] in ['top','bottom'],'VM copper stays on outer layers'
assert not math.isclose(width,.5,abs_tol=1e-6),'no reduced 0.50 mm VM segment remains'
if width>=.8-1e-6:lines[a['layer']].append(LineString([(a['x'],a['y']),(b['x'],b['y'])]))
trace_sources={e['pcb_trace_id']:e.get('source_trace_id') for e in c if e['type']=='pcb_trace'}
vias=[]
for v in c:
if v['type']!='pcb_via':continue
sid=v.get('source_trace_id',trace_sources.get(v.get('pcb_trace_id')))
if sid in sources and snet(sources[sid])==vm:vias.append(v)
graph=defaultdict(dict)
def node(l,p):return l,round(p[0],6),round(p[1],6)
def add(a,b,cost):graph[a][b]=cost;graph[b][a]=cost
for l,items in lines.items():
union=unary_union(items);ls=[union] if union.geom_type=='LineString' else list(union.geoms)
for line in ls:
for a,b in zip(list(line.coords),list(line.coords)[1:]):
segment=LineString([a,b]);cuts=[(0,a),(segment.length,b)]
for v in vias:
p=Point(v['x'],v['y'])
if segment.distance(p)<1e-6:cuts.append((segment.project(p),(v['x'],v['y'])))
cuts.sort()
for (_,a),(_,b) in zip(cuts,cuts[1:]):
if math.dist(a,b)>1e-8:add(node(l,a),node(l,b),math.dist(a,b))
for v in vias:
assert v['hole_diameter']==.3 and v['outer_diameter']==.45
assert set(v['layers'])==set(LAYERS)
a,b=node('top',(v['x'],v['y'])),node('bottom',(v['x'],v['y']))
if a in graph and b in graph:add(a,b,1.6)
start=node('bottom',(-1.8,-16));end=node('top',(7,-16))
queue=[(0,start)];costs={start:0};previous={}
while queue:
cost,a=heapq.heappop(queue)
if cost>costs[a]+1e-8:continue
if a==end:break
for b,d in graph[a].items():
value=cost+d
if value<costs.get(b,math.inf):costs[b]=value;previous[b]=a;heapq.heappush(queue,(value,b))
assert end in costs,'real >=0.80 mm outer path must join exact former crossbar endpoints'
route=[end]
while route[-1] in previous:route.append(previous[route[-1]])
route.reverse()
wire_length=sum(math.dist(a[1:],b[1:]) for a,b in zip(route,route[1:]) if a[0]==b[0])
transitions=sum(a[0]!=b[0] for a,b in zip(route,route[1:]))
assert wire_length<15,'bounded local VM repair, not a whole-board power detour'
def widths(name):
sid=by_name[name]['source_trace_id']
traces=[e for e in c if e['type']=='pcb_trace' and e.get('source_trace_id')==sid]
assert traces
return [p['width'] for t in traces for p in t['route'] if p['route_type']=='wire']
assert min(widths('DRV_ISEN_B'))>=.3-1e-6
assert all(p['layer'] in ['top','bottom'] for t in c if t['type']=='pcb_trace' and t.get('source_trace_id')==by_name['DRV_ISEN_B']['source_trace_id'] for p in t['route'] if p['route_type']=='wire')
assert min(widths('ETH_RAIL_BC_C'))>=.25-1e-6
assert min(widths('ETH_RAIL_C_C7'))>=by_name['ETH_RAIL_C_C7']['min_trace_thickness']-1e-6
warning=next(e for e in c if e['type']=='pcb_trace_warning' and e.get('source_trace_id')==by_name['ETH_RAIL_BC_C']['source_trace_id'])
flagged=next(e for e in c if e['type']=='pcb_trace' and e['pcb_trace_id']==warning['pcb_trace_id'])
assert flagged['source_trace_id']==by_name['ETH_RAIL_C_C7']['source_trace_id'],'Ethernet diagnostic refers to a different, separately constrained branch'
report=dict(circuitSha256=hashlib.sha256(path.read_bytes()).hexdigest(),vmBridgeMinimumMm=.8,vmBridgeOuterOnly=True,
formerCrossbarEndpoints=[[-1.8,-16],[7,-16]],actualEmittedBridgePath=route,wireLengthMm=wire_length,throughViaTransitions=transitions,
reducedHalfMmVmSegmentsRemaining=0,phaseBReturnMinimumMm=min(widths('DRV_ISEN_B')),
ethernetMainMinimumMm=min(widths('ETH_RAIL_BC_C')),ethernetMainRequestedMm=by_name['ETH_RAIL_BC_C']['min_trace_thickness'],
ethernetCapBranchMinimumMm=min(widths('ETH_RAIL_C_C7')),ethernetCapBranchRequestedMm=by_name['ETH_RAIL_C_C7']['min_trace_thickness'],
ethernetWarningPhysicalRegression=False,warningStillReported=True,
scope='Geometric/width and connectivity evidence, not load, thermal or signal-integrity bench qualification.')
Path(os.environ.get('WIDTH_REPORT_OUTPUT','reports/round10-width-proof.json')).write_text(json.dumps(report,indent=2)+'\n')
print(json.dumps(report))