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-software-current-copper.py
"""Compare actual emitted metal to v0.4.55, with explicit local rework only."""
from route_geometry import *
from shapely.ops import unary_union
from collections import defaultdict
import hashlib
before_path=Path('baseline/v055-circuit.json');after_path=Path('dist/index/circuit.json')
before=load_circuit(str(before_path));after=load_circuit(str(after_path))
assert any(e['type']=='source_component' and e['name']=='U_CURRENT_DAC' for e in after),'Do not audit the inherited old dist as the new board'
oldmap=json.loads(Path('../../work/nema17-clean-schematic-v054-IAQpFd/routing/net-map.json').read_text())['roots']
newmap=json.loads(Path('routing/net-map.json').read_text())['roots']
removed={
'N101':{frozenset([(6.4,-13.55),(-2.4,-13.55)]),frozenset([(-2.4,-13.55),(-2.7,-12.55)]),frozenset([(-2.7,-12.55),(-4.15,-12.55)])},
'N204':{frozenset([(-1.4827,-11.7985),(-4.1963,-14.5221)])},
'N168':{frozenset([(5.75,-16),(5.75,-13.95)]),frozenset([(5.75,-13.95),(8.05,-11.65)])},
'N118':{frozenset([(3.2499,-9.2861),(4.3638,-10.4)]),frozenset([(4.3638,-10.4),(5.1,-10.4)]),frozenset([(5.1,-10.4),(5.7,-9.8)])},
}
def collect(config):
c,m,is_before=config['c'],config['map'],config['before'];r=connectivity(c)
src={e['source_trace_id']:e for e in c if e['type']=='source_trace'}
traces={e['pcb_trace_id']:e for e in c if e['type']=='pcb_trace'}
ports={e['pcb_port_id']:e for e in c if e['type']=='pcb_port'}
parts={e['source_component_id']:e['name'] for e in c if e['type']=='source_component'}
pc={e['pcb_component_id']:parts[e['source_component_id']] for e in c if e['type']=='pcb_component'}
moved={'R_REF_TOP','C_U_2V5_IN','C_ETH_1'}
metal=defaultdict(list);wide_ground=defaultdict(list);moved_lands=defaultdict(list)
for e in c:
if e['type'] not in ['pcb_smtpad','pcb_plated_hole']:continue
p=ports.get(e.get('pcb_port_id'))
if not p:continue
n=m.get(r(p['source_port_id']));g=geometry(e,'outer_' if e['type']=='pcb_plated_hole' else '')
for layer in e.get('layers',[e.get('layer')]):
if is_before and pc[e['pcb_component_id']] in moved:
moved_lands[n,layer].append(g)
else:metal[n,layer].append(g)
if not is_before and n=='N62':wide_ground[layer].append(g)
for e in c:
s=src.get(e.get('source_trace_id',traces.get(e.get('pcb_trace_id'),{}).get('source_trace_id')))
if not s:continue
ids=s.get('connected_source_port_ids',[])+s.get('connected_source_net_ids',[])
n=m.get(r(ids[0]),'NC_'+r(ids[0]))
if is_before and s.get('name')=='DRV_REF_FEED':continue
if e['type']=='pcb_trace':
for a,b in zip(e['route'],e['route'][1:]):
if a['route_type']!=b['route_type'] or a['route_type']!='wire' or a['layer']!=b['layer']:continue
xy=[(a['x'],a['y']),(b['x'],b['y'])]
if math.dist(*xy)<1e-8:continue
if is_before and frozenset(xy) in removed.get(n,set()):continue
w=max(a['width'],b['width']);g=LineString(xy).buffer(w/2,quad_segs=16)
metal[n,a['layer']].append(g)
if n=='N62' and w>=.275:wide_ground[a['layer']].append(g)
elif e['type']=='pcb_via':
g=Point(e['x'],e['y']).buffer(e['outer_diameter']/2,quad_segs=16)
for layer in LAYERS:
metal[n,layer].append(g)
if not is_before and n=='N62':wide_ground[layer].append(g)
union={k:unary_union(v) for k,v in metal.items()}
wide={k:unary_union(v) for k,v in wide_ground.items()}
# Imported route contacts can be pruned/represented differently inside
# actual SMT lands. Compare manufactured metal, not track centerlines
# without their lands. Only the old land areas of the three explicitly
# approved component moves are excluded; outside them all old wide copper
# and unrelated net/layer geometry remain strictly protected.
for key,gs in moved_lands.items():
g=unary_union(gs)
if key in union:union[key]=union[key].difference(g)
if key[0]=='N62' and key[1] in wide:wide[key[1]]=wide[key[1]].difference(g)
return union,wide
a,ag=collect(dict(c=before,map=oldmap,before=True));b,bg=collect(dict(c=after,map=newmap,before=False))
# Ground/reference are deliberately rebuilt locally; their complete port
# continuity, widths, drill rules and physical fields have separate gates.
# No exemption applies to unrelated nets or their layer geometry.
rebuilt={'N62','N189','N215'}
additions={'N2','N101','N118','N193','N204','N168','N161','N162'}
fail=[];protected=[]
for key,g in a.items():
n,layer=key
if n in rebuilt:continue
h=b.get(key,Polygon())
if not h.buffer(.0002).covers(g):
lost=g.difference(h.buffer(.0002));fail.append(dict(net=n,layer=layer,kind='lost_original_metal',areaMm2=lost.area,bounds=lost.bounds))
if n not in additions and not g.buffer(.0002).covers(h):fail.append(dict(net=n,layer=layer,kind='changed_unrelated_metal',areaMm2=h.difference(g.buffer(.0002)).area))
protected.append(key)
for key in b.keys()-a.keys():
if key[0] not in rebuilt|additions:fail.append(dict(net=key[0],layer=key[1],kind='new_unrelated_net_layer'))
for layer,g in ag.items():
if not bg.get(layer,Polygon()).buffer(.0002).covers(g):
fail.append(dict(net='N62',layer=layer,kind='lost_wide_ground_copper',areaMm2=g.difference(bg.get(layer,Polygon()).buffer(.0002)).area))
report=dict(circuitSha256=hashlib.sha256(after_path.read_bytes()).hexdigest(),baselineSha256=hashlib.sha256(before_path.read_bytes()).hexdigest(),
protectedNetLayers=len(protected),toleranceMm=.0002,unintendedChanges=fail,
comparisonIncludesActualLands=True,approvedMovedLandAreas=['R_REF_TOP','C_U_2V5_IN','C_ETH_1'],
explicitRework='DAC reference, low-current local ground, same-width status/enable elbows, retired obsolete C_ETH_1 fanout replaced by a shorter 0.25 mm connection, declared 0.50 mm input-feed detour; original winding/high-current copper outside the three approved former land areas and all unrelated nets protected')
Path('reports/software-current-copper-regression.json').write_text(json.dumps(report,indent=2)+'\n')
print(json.dumps(report));assert not fail