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/repair-round9-motor.py
"""Candidate-only coordinated motor escape repair; no rule or via-size changes.
The VM trunk moves around the two shunt escape sites, not to an inner layer.
The A+ and 3V3 stitches must vacate its new corridor. Generated copper is
independently checked before it can replace the active replay.
"""
from route_geometry import *
import copy,sys
base=Path('../../checkpoints/nema17-round8-validated-local-20261020-ii8Wrw/restore-verified')
c=json.loads((base/'dist/index/circuit.json').read_text())
routes=json.loads((base/'routing/complete-routes.json').read_text())
source={e['source_trace_id']:e for e in c if e['type']=='source_trace'}
root=connectivity(c)
part_moves={'R_ISEN_B':dict(x=3.9,y=-15.6,rotation=90,layer='bottom')}
for name,move in part_moves.items():
s=next(e for e in c if e['type']=='source_component' and e['name']==name)
p=next(e for e in c if e['type']=='pcb_component' and e['source_component_id']==s['source_component_id'])
dx,dy=move['x']-p['center']['x'],move['y']-p['center']['y']
assert p['rotation']==move['rotation'] and p['layer']==move['layer']
for e in c:
if e is not p and e.get('pcb_component_id')!=p['pcb_component_id']:continue
if e['type']=='pcb_trace':continue
for field in ['center','anchor_position']:
if field in e:e[field]['x']+=dx;e[field]['y']+=dy
if 'x' in e:e['x']+=dx;e['y']+=dy
for field in ['points','outline']:
for q in e.get(field,[]):q['x']+=dx;q['y']+=dy
def net(t):
s=source[t['source_trace_id']]
return root((s['connected_source_port_ids']+s['connected_source_net_ids'])[0])
def near(a,b):return math.dist(a,b)<1e-6
def xy(p):return p['x'],p['y']
vm=net({'source_trace_id':'source_trace_305'})
detour=[(-1.8,-16),(-1.1,-16),(-.3,-16.8),(2.8,-16.8),(3.6,-16),(7,-16)]
replaced=0
for t in routes:
if net(t)!=vm:continue
result=[]
for a,b in zip(t['route'],t['route'][1:]):
result.append(a)
if a['route_type']==b['route_type']=='wire' and a['layer']==b['layer']=='bottom':
points=detour if near(xy(a),detour[0]) and near(xy(b),detour[-1]) else list(reversed(detour)) if near(xy(b),detour[0]) and near(xy(a),detour[-1]) else None
if points:
assert max(a['width'],b['width'])==.8
result.extend({**a,'x':x,'y':y} for x,y in points[1:-1]);replaced+=1
result.append(t['route'][-1]);t['route']=result
assert replaced>=1,'expected original 0.8 mm VM trunk'
def move_via(old,new,layer_paths):
matches=0
for t in routes:
# All repeated physical occurrences move, not only one source path.
route=t['route'];out=[]
for p in route:
if p['route_type']=='via' and near(xy(p),old):
matches+=1
for layer,reverse in [(p['from_layer'],False),(p['to_layer'],True)]:
if reverse:out.append({**p,'x':new[0],'y':new[1]})
points=layer_paths[layer]
if reverse:points=list(reversed(points))
out.extend(dict(route_type='wire',layer=layer,width=.3,x=x,y=y) for x,y in points)
else:out.append(p)
t['route']=out
assert matches,('missing via',old)
# Moving this A+ stitch down keeps its top-side approach in the same open
# channel. Its old bottom toe is retired, rather than left crossing new VM.
old=(-.65,-16.8);new=(-.65,-17.9)
anet=net({'source_trace_id':'source_trace_353'})
for t in routes:
if net(t)!=anet:continue
for p in t['route']:
if p['route_type']=='wire' and p['layer']=='bottom' and near(xy(p),old):p['x'],p['y']=new
move_via(old,new,{'top':[old,new],'bottom':[new]})
move_via((.2,-14.35),(.2249,-15.6),{'top':[(.2,-14.35),(.2249,-15.6)],'bottom':[(.2,-14.35),(.2249,-15.6)]})
move_via((2.2,-14.35),(2.1751,-15.6),{'top':[(2.2,-14.35),(2.1751,-15.6)],'bottom':[(2.2,-14.35),(2.1751,-15.6)]})
# The B shunt moves to a clear bottom-side site. Retire its entire old
# crossing lead and ground leaf; rebuild both from the actual new pads.
ground=next(t for t in routes if t['source_trace_id']=='source_trace_337')
routes=[t for t in routes if not(t['source_trace_id'] in ['source_trace_336','source_trace_337'] and t['route'][0].get('layer')=='bottom')]
def terminal(name,pin):
s=next(e for e in c if e['type']=='source_component' and e['name']==name)
p=next(e for e in c if e['type']=='source_port' and e['source_component_id']==s['source_component_id'] and e['pin_number']==pin)
return next(e for e in c if e['type']=='pcb_port' and e['source_port_id']==p['source_port_id'])
bp,bg=terminal('R_ISEN_B',1),terminal('R_ISEN_B',2)
owner=next(t for t in routes if t['source_trace_id']=='source_trace_336')
routes.append({**{k:v for k,v in owner.items() if k!='route'},'pcb_trace_id':'round9_B_shunt_positive',
'route':[dict(route_type='wire',layer='bottom',width=.3,x=x,y=y) for x,y in [(2.1751,-15.6),(bp['x'],bp['y'])]]})
routes.append({**{k:v for k,v in ground.items() if k!='route'},'pcb_trace_id':'round9_B_shunt_ground',
'route':[dict(route_type='wire',layer='bottom',width=.4,x=x,y=y) for x,y in [(bg['x'],bg['y']),(5.6,-16.85)]]})
def circuit_from_routes(routes):
result=[e for e in c if e['type'] not in ['pcb_trace','pcb_via','pcb_copper_pour']]
result+=copy.deepcopy(routes)
unique={}
for t in routes:
for p in t['route']:
if p['route_type']=='via':
k=tuple(round(v,6) for v in xy(p))
if k in unique:assert net(t)==unique[k]['net'];continue
unique[k]=dict(net=net(t),trace=t,p=p)
for (x,y),v in unique.items():
t=v['trace']
result.append(dict(type='pcb_via',pcb_via_id=f'round9_stage_via_{len(result)}',
pcb_trace_id=t['pcb_trace_id'],source_trace_id=t['source_trace_id'],
subcircuit_id=t['subcircuit_id'],x=x,y=y,hole_diameter=.3,outer_diameter=.45,
layers=LAYERS,tented_on_top=True,tented_on_bottom=True))
native=json.loads(Path('routing/native-features.json').read_text())
fixed={(v['x'],v['y']) for v in native['thermal_vias']+native.get('auxiliary_vias',[])}
result += [copy.deepcopy(e) for e in c if e['type']=='pcb_via' and (e['x'],e['y']) in fixed and (e['x'],e['y']) not in unique]
return result
if len(sys.argv)>1 and not sys.argv[1].startswith('--'):
routes=json.loads(Path(sys.argv[1]).read_text())
if '--remove-supply-stitch' in sys.argv:
# This is a candidate removal, not a synthesized layer bridge. The
# physical graph must prove both rail islands meet via another real via.
for t in routes:
t['route']=[p for p in t['route'] if not(p['route_type']=='via' and near(xy(p),(0,-16.8)))]
owner=next(t for t in routes if t['source_trace_id']=='source_trace_442')
routes.append({**{k:v for k,v in owner.items() if k!='route'},'pcb_trace_id':'round9_3v3_existing_stitch_bridge',
'route':[dict(route_type='wire',layer='inner1',width=.15,x=x,y=y) for x,y in [(0,-16.8),(7.85,-18.175)]]})
Path('routing/round9-placement.json').write_text(json.dumps(part_moves,indent=2)+'\n')
Path('routing/round9-motor-stage.json').write_text(json.dumps(routes,indent=2)+'\n')
Path('reports/round9-motor-stage.circuit.json').write_text(json.dumps(circuit_from_routes(routes),indent=2)+'\n')
print(json.dumps(dict(candidateOnly=True,vmTrunkOccurrencesReplaced=replaced,paths=len(routes))))