375 行
20 KiB
Python
375 行
20 KiB
Python
"""Create R2 functional placement from schematic connectivity and mechanical anchors.
|
|
|
|
Run with KiCad Python. The source snapshot is the unrouted native KiCad board;
|
|
no coordinates, copper, or routes are read from the legacy layout.
|
|
"""
|
|
from collections import defaultdict
|
|
import hashlib
|
|
import json
|
|
import math
|
|
from pathlib import Path
|
|
import shutil
|
|
|
|
import pcbnew as pcb
|
|
|
|
from bootstrap_pcb import ROOT, CONFIG, xy, keepout, label, pth_apertures
|
|
from schematic_maintenance import parse, children, child
|
|
|
|
OUT = ROOT / 'out/pcb-layout-r2'
|
|
REVISION = 'R2-FUNCTIONAL-PLACEMENT'
|
|
|
|
|
|
def digest(path):
|
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
|
|
|
|
def box_mm(item):
|
|
b = item.GetBoundingBox(False, False) if isinstance(item, pcb.FOOTPRINT) else item.GetBoundingBox()
|
|
return tuple(pcb.ToMM(v) for v in (b.GetLeft(), b.GetTop(), b.GetRight(), b.GetBottom()))
|
|
|
|
|
|
def expand(box, d):
|
|
a, b, c, e = box
|
|
return a-d, b-d, c+d, e+d
|
|
|
|
|
|
def intersects(a, b):
|
|
return a[0] < b[2] and b[0] < a[2] and a[1] < b[3] and b[1] < a[3]
|
|
|
|
|
|
def point(item):
|
|
q = item.GetPosition()
|
|
return pcb.ToMM(q.x), pcb.ToMM(q.y)
|
|
|
|
|
|
class Layout:
|
|
def __init__(self, number):
|
|
self.number = number
|
|
self.name = f'PhoneRail-Board{number}'
|
|
self.path = ROOT / self.name / (self.name + '.kicad_pcb')
|
|
text = (OUT / 'before' / self.path.name).read_text(encoding='utf-8')
|
|
edits = [(node.start,node.end) for node in children(parse(text),'gr_text')
|
|
if float(child(node,'at')[1]) > 270 or 'UNROUTED' in node[1]]
|
|
for start,end in sorted(edits,reverse=True):
|
|
text = text[:start]+text[end:]
|
|
source = OUT / ('input-'+self.path.name)
|
|
source.write_text(text,encoding='utf-8')
|
|
self.board = pcb.LoadBoard(str(source))
|
|
assert not list(self.board.GetTracks()), 'This placement tool requires an unrouted source'
|
|
self.parts = {f.GetReference(): f for f in self.board.GetFootprints()}
|
|
self.done = {r for r, f in self.parts.items() if f.IsLocked()}
|
|
self.obstacles = []
|
|
self.placements = {}
|
|
for r in self.done:
|
|
f = self.parts[r]
|
|
both = any(p.GetAttribute() in (pcb.PAD_ATTRIB_PTH, pcb.PAD_ATTRIB_NPTH) for p in f.Pads())
|
|
self.obstacles.append((r, expand(box_mm(f), .35), -1 if both else f.GetLayer()))
|
|
for z in self.board.Zones():
|
|
if z.GetIsRuleArea() and z.GetDoNotAllowFootprints():
|
|
self.obstacles.append((z.GetZoneName(), expand(box_mm(z), .25),
|
|
-1 if z.GetLayerSet().Contains(pcb.F_Cu) else pcb.B_Cu))
|
|
|
|
def reserve(self, name, rect, layers, copper=False, footprints=True):
|
|
keepout(self.board, name, rect, layers, copper)
|
|
z = list(self.board.Zones())[-1]
|
|
z.SetDoNotAllowFootprints(footprints)
|
|
if footprints:
|
|
self.obstacles.append((name, rect, -1 if len(layers) > 1 else layers[0]))
|
|
|
|
def put(self, ref, target, rotation=0, radius=8, step=.5, bottom=None, reason='functional placement'):
|
|
assert ref not in self.done, (ref, 'already placed')
|
|
f = self.parts[ref]
|
|
bottom = self.number == 2 if bottom is None else bottom
|
|
if f.IsFlipped() != bottom:
|
|
f.Flip(f.GetPosition(), pcb.FLIP_DIRECTION_TOP_BOTTOM)
|
|
rotations = rotation if isinstance(rotation, tuple) else (rotation,)
|
|
layer = pcb.B_Cu if bottom else pcb.F_Cu
|
|
candidates = [(0, 0)]
|
|
for k in range(1, int(radius / step)+1):
|
|
candidates.extend((dx*step, dy*step) for dx in range(-k, k+1) for dy in (-k, k))
|
|
candidates.extend((dx*step, dy*step) for dy in range(-k+1, k) for dx in (-k, k))
|
|
candidates.sort(key=lambda q: q[0]**2+q[1]**2)
|
|
winner = None
|
|
for dx, dy in candidates:
|
|
for rot in rotations:
|
|
f.SetPosition(xy(target[0]+dx, target[1]+dy))
|
|
f.SetOrientationDegrees(rot)
|
|
b = box_mm(f)
|
|
if b[0] < 51 or b[1] < 51 or b[2] > (271 if ref == 'J22' else 269) or b[3] > 184:
|
|
continue
|
|
if any((ol == -1 or ol == layer) and intersects(b, ob) for _, ob, ol in self.obstacles):
|
|
continue
|
|
winner = (b, rot)
|
|
break
|
|
if winner:
|
|
break
|
|
if winner is None:
|
|
raise RuntimeError(f'No legal location for {ref} near {target}')
|
|
b, rot = winner
|
|
f.SetLocked(False)
|
|
f.SetField('Placement', REVISION)
|
|
f.SetField('LayoutIntent', reason)
|
|
for field in f.GetFields():
|
|
if field.GetName() not in {'Reference', 'Value'}:
|
|
field.SetVisible(False)
|
|
f.Value().SetVisible(False)
|
|
f.Reference().SetLayer(pcb.B_SilkS if bottom else pcb.F_SilkS)
|
|
f.Reference().SetMirrored(bottom)
|
|
f.Reference().SetTextAngle(pcb.EDA_ANGLE(0, pcb.DEGREES_T))
|
|
f.Reference().SetTextSize(xy(.8, .8))
|
|
f.Reference().SetTextThickness(pcb.FromMM(.12))
|
|
f.Reference().SetPosition(xy((b[0]+b[2])/2, b[1]-.7))
|
|
self.done.add(ref)
|
|
both = any(p.GetAttribute() == pcb.PAD_ATTRIB_PTH for p in f.Pads())
|
|
self.obstacles.append((ref, expand(b, .45), -1 if both else layer))
|
|
self.placements[ref] = {'xy_mm': point(f), 'rotation': rot, 'layer': self.board.GetLayerName(layer),
|
|
'target_displacement_mm': round(math.hypot(dx,dy),3), 'intent': reason}
|
|
return f
|
|
|
|
def near(self, ref, anchor, pin=None, offset=(0,0), **kwargs):
|
|
f = self.parts[anchor]
|
|
obj = next(p for p in f.Pads() if p.GetNumber() == str(pin)) if pin else f
|
|
x, y = point(obj)
|
|
return self.put(ref, (x+offset[0], y+offset[1]), rotation=(0,90,180,270), **kwargs)
|
|
|
|
def finish(self):
|
|
missing = set(self.parts) - self.done
|
|
assert not missing, sorted(missing)
|
|
self.board.GetTitleBlock().SetRevision(REVISION)
|
|
self.board.GetTitleBlock().SetTitle(f'PhoneRail Board {self.number} - functional placement / unrouted')
|
|
label(self.board, f'BOARD {self.number} R2 - PLACED / UNROUTED', 160, 46, 1.5)
|
|
label(self.board, 'F.Cu: horizontal | B.Cu: vertical | local escapes require review', 160, 189, 1)
|
|
place_silkscreen(self.board)
|
|
candidate = OUT / self.path.name
|
|
pcb.SaveBoard(str(candidate), self.board)
|
|
candidate.write_text(pth_apertures(candidate.read_text(encoding='utf-8')), encoding='utf-8')
|
|
return self.board
|
|
|
|
|
|
def place_silkscreen(board, refs=None):
|
|
# Fit references in actual free board space without suppressing DRC severities.
|
|
occupied = defaultdict(list)
|
|
for f in board.GetFootprints():
|
|
for p in f.Pads():
|
|
for copper, silk in ((pcb.F_Cu,pcb.F_SilkS),(pcb.B_Cu,pcb.B_SilkS)):
|
|
if p.GetLayerSet().Contains(copper):
|
|
occupied[silk].append(expand(box_mm(p), .2))
|
|
for shape in f.GraphicalItems():
|
|
if shape.GetLayer() in (pcb.F_SilkS, pcb.B_SilkS):
|
|
occupied[shape.GetLayer()].append(expand(box_mm(shape), .12))
|
|
if refs is not None and f.GetReference() not in refs and f.Reference().IsVisible():
|
|
occupied[f.Reference().GetLayer()].append(expand(box_mm(f.Reference()), .12))
|
|
for f in sorted(board.GetFootprints(), key=lambda x:x.GetReference()):
|
|
t = f.Reference()
|
|
if not t.IsVisible() or (refs is not None and f.GetReference() not in refs):
|
|
continue
|
|
layer = t.GetLayer()
|
|
b = box_mm(f)
|
|
cx, cy = (b[0]+b[2])/2, (b[1]+b[3])/2
|
|
t.SetTextAngle(pcb.EDA_ANGLE(0,pcb.DEGREES_T))
|
|
found = False
|
|
for d in (.8,1.3,1.8,2.5,3.5,5,7):
|
|
positions = [(cx,b[1]-d),(cx,b[3]+d),(b[0]-d-1.5,cy),(b[2]+d+1.5,cy)]
|
|
positions += [(cx+dx,b[1]-d) for dx in (-3,-2,-1,1,2,3)]
|
|
positions += [(cx+dx,b[3]+d) for dx in (-3,-2,-1,1,2,3)]
|
|
for x,y in positions:
|
|
t.SetPosition(xy(x,y))
|
|
tb = box_mm(t)
|
|
if tb[0]<51 or tb[2]>269 or tb[1]<51 or tb[3]>184:
|
|
continue
|
|
if not any(intersects(tb, q) for q in occupied[layer]):
|
|
occupied[layer].append(expand(tb,.12))
|
|
found = True
|
|
break
|
|
if found:
|
|
break
|
|
if not found:
|
|
t.SetLayer(pcb.B_Fab if f.IsFlipped() else pcb.F_Fab)
|
|
t.SetPosition(xy(cx,cy))
|
|
f.SetField('ReferenceOnFabOnly','Dense area: refer to assembly drawing')
|
|
for field in f.GetFields():
|
|
if field.GetName()=='ReferenceOnFabOnly':
|
|
field.SetVisible(False)
|
|
field.SetMirrored(f.IsFlipped())
|
|
|
|
|
|
def carrier():
|
|
s = Layout(2)
|
|
# Bottom-side electronics stay clear of the lower board's outward-facing antenna.
|
|
s.reserve('R2_ESP32_ANTENNA_PROJECTION', (232,175,258,184), [pcb.F_Cu,pcb.In1_Cu,pcb.In2_Cu,pcb.B_Cu], True)
|
|
s.put('U32',(174,122),90,reason='Hub centered between five ports; upstream exit faces east')
|
|
s.near('C749','U32',57,offset=(2,-2),radius=3,reason='Prioritize hub bypass beside upstream PHY supply pin')
|
|
s.put('U31',(152,122),0,reason='Hub LDO between BTB supply and hub')
|
|
s.put('U21',(203,130),0,reason='595 serial input adjacent to J32 control pins')
|
|
s.put('U22',(184,135),0,reason='595 chain toward lower phone sites')
|
|
s.put('U33',(164,137),0,reason='595 chain end and hub reset')
|
|
s.put('U34',(239,154),0,reason='Carrier temperature sensor away from LDO and bulk capacitors')
|
|
for ch in range(1,6):
|
|
# Rotate this functional cell with the mechanical phone orientation.
|
|
sign = 1 if ch <= 3 else -1
|
|
bat, tail = f'J8{ch}',f'J7{ch}'
|
|
bx,by = point(s.parts[bat])
|
|
tx,ty = point(s.parts[tail])
|
|
base = 800+(ch-1)*10
|
|
s.put(f'C{base+1}',(bx+sign*12,by+sign*2),0, radius=12,
|
|
reason='10.5mm bulk capacitor beside battery; lower-board projection reserved')
|
|
for k, offset in [(2,(7,-5)),(3,(7,-2)),(4,(7,1)),(5,(7,4)),(6,(5,-6))]:
|
|
s.put(f'C{base+k}',(bx+sign*offset[0],by+sign*offset[1]),(0,90,180,270),radius=10,
|
|
reason='Battery ceramic bank outside ribbon lane')
|
|
for ref,pin in [(f'R{780+ch}',3),(f'R{790+ch}',4)]:
|
|
s.near(ref,bat,pin,reason='Battery identification resistor at connector')
|
|
s.put(f'U{50+ch}',(tx+sign*3,ty-sign*5),0,radius=8,
|
|
reason='USB ESD flow-through device adjacent to tail connector')
|
|
s.put(f'U{23+ch}',(tx+sign*10,ty-sign*7),0,radius=10,
|
|
reason='Per-port VBUS switch near tail; bulk current separated from USB pair')
|
|
for prefix,pin in [('C70',5),('C71',1),('C72',4),('R70',6)]:
|
|
ref = f'{prefix}{ch}'
|
|
if ref in s.parts:
|
|
s.near(ref,f'U{23+ch}',pin,reason='VBUS switch local decoupling/timing/default state')
|
|
for ref,pin in [(f'R{760+ch}',22),(f'R{770+ch}',14)]:
|
|
s.near(ref,tail,pin,reason='Tail accessory termination')
|
|
# Keep EDL service connector reachable from the side of each site.
|
|
s.put(f'J6{ch}',(bx-sign*10,by-sign*9),0,radius=10,reason='EDL service connector beside battery site')
|
|
s.near(f'U4{ch}',f'J6{ch}',offset=(0,-sign*6),radius=10,reason='EDL analog switch at service connector')
|
|
s.near(f'C{760+ch}',f'U4{ch}',5,reason='EDL switch supply bypass')
|
|
s.near(f'R{740+ch}',f'U4{ch}',6,reason='EDL enable defaults low')
|
|
for k in range(3):
|
|
q = f'Q{ch}{k+3:02}'
|
|
contact = f'J{41+(ch-1)*4+k}'
|
|
s.near(q,contact,offset=(-sign*3,sign*7),radius=14,reason='Key MOS close to its phone contact')
|
|
for suffix in (20+2*k,21+2*k):
|
|
s.near(f'R{ch}{suffix}',q,1,reason='Key gate resistor/pulldown at MOS')
|
|
for ref,anchor,pin in [('C736','U31',3),('C737','U31',2),('C760','U34',8),
|
|
('C604','U21',16),('C605','U22',16),('C739','U33',16),('R609','U21',13)]:
|
|
s.near(ref,anchor,pin,reason='Supply bypass / control bias at IC pin')
|
|
s.near('Y1','U32',61,offset=(0,-4),radius=8,reason='24MHz crystal immediately at hub oscillator pins')
|
|
for ref,pin in [('C740',1),('C741',3)]:
|
|
s.near(ref,'Y1',pin,reason='Crystal load capacitor; isolated ground return')
|
|
hub_pins = {'C742':25,'C743':62,'C744':5,'C745':10,'C746':24,'C747':46,'C748':52,
|
|
'C750':64,'C751':64,'R708':63,'R709':44,'R710':44,'R711':41,
|
|
'R712':42,'R714':54,'R715':56,'R716':43,'R719':40,'R720':45}
|
|
for ref,pin in hub_pins.items():
|
|
s.near(ref,'U32',pin,radius=12,reason='Hub pin-local bypass or configuration resistor')
|
|
s.near('R717','J23','A5',offset=(-5,0),reason='USB-C CC termination')
|
|
s.near('R718','J23','B5',offset=(-5,0),reason='USB-C CC termination')
|
|
s.near('R721','J23','A4B9',offset=(-8,0),reason='Upstream VBUS detection divider')
|
|
s.near('R722','R721',2,reason='Upstream VBUS detection divider return')
|
|
return s
|
|
|
|
|
|
def power_cell(s,ch,origin):
|
|
x,y = origin
|
|
buck, efuse, ina = f'U{ch}',f'U{ch+13}',f'U{ch+7}'
|
|
s.put(buck,(x,y),0,radius=0,reason=f'CH{ch} buck; VIN west, SW east, feedback south')
|
|
s.put(f'L{ch}01',(x+7,y),0,radius=1,reason='Inductor directly at SW; horizontal hot path')
|
|
s.put(f'C{ch}06',(x+14,y-2),90,radius=2,reason='First buck output capacitor immediately after inductor')
|
|
s.put(f'C{ch}07',(x+17,y-2),90,radius=2,reason='Parallel buck output capacitor')
|
|
s.put(efuse,(x+20,y+5),90,radius=3,reason='Output protection between buck capacitor and shunt')
|
|
s.put(f'Rs{ch}01',(x+26,y+9),180,radius=3,reason='4A output shunt; Kelvin pickup on inner pad edges')
|
|
s.put(ina,(x+26,y+15),0,radius=3,reason='INA inputs face shunt; digital bus exits away from power path')
|
|
for k,dy in [(1,-2),(2,1),(3,4),(4,-4)]:
|
|
s.put(f'C{ch}{k:02}',(x-5,y+dy),(0,180,90,270),radius=5,reason='Local VIN ceramic hot-loop bank')
|
|
s.near(f'C{ch}05',buck,13,offset=(2,-2),radius=5,reason='Bootstrap capacitor directly between BOOT and SW')
|
|
s.near(f'C{ch}08',efuse,11,reason='eFuse input bypass')
|
|
for ref,pin in [(f'R{ch}01',7),(f'R{ch}02',7),(f'R{ch}03',1),(f'R{ch}04',10),
|
|
(f'R{ch}05',10),(f'R{ch}06',8),(f'C{ch}09',9),(f'C{ch}10',8),
|
|
(f'C{ch}11',8),(f'R{ch}07',7)]:
|
|
s.near(ref,buck,pin,radius=9,reason='Buck quiet feedback/compensation/soft-start component')
|
|
s.near(f'C{ch}12',f'R{ch}07',2,reason='DAC injection low-pass at feedback branch')
|
|
s.near(f'R{ch}08',f'C{ch}12',1,reason='DAC injection series resistor')
|
|
for k,pin in [(9,15),(10,15),(11,17),(12,19),(13,20),(14,14),(15,2),(16,3),(17,3)]:
|
|
s.near(f'R{ch}{k:02}',efuse,pin,radius=10,reason='eFuse OVP/ILIM/PG threshold or default bias')
|
|
s.near(f'C{ch}14',efuse,18,reason='eFuse ramp timing capacitor')
|
|
s.near(f'C{ch}13',ina,6,reason='INA226 local supply bypass')
|
|
s.near(f'Q{ch}01',efuse,14,offset=(-4,4),reason='Discharge interlock local to eFuse enable')
|
|
s.near(f'Q{ch}02',f'Q{ch}01',3,offset=(0,5),reason='Discharge MOS isolated from quiet feedback')
|
|
s.near(f'R{ch}18',f'Q{ch}01',3,reason='Discharge gate bias')
|
|
s.near(f'R{ch}19',f'Q{ch}02',3,offset=(5,4),radius=10,reason='Pulse discharge resistor with open thermal surroundings')
|
|
|
|
|
|
def lower(upper):
|
|
s = Layout(1)
|
|
for ch in range(1,6):
|
|
ref = f'C{801+(ch-1)*10}'
|
|
s.reserve(f'R2_HEIGHT_{ref}_10.5mm',expand(box_mm(upper.parts[ref]),1),[pcb.F_Cu])
|
|
s.reserve('R2_RF_COPPER_VOID',(232,175,258,184),[pcb.F_Cu,pcb.In1_Cu,pcb.In2_Cu,pcb.B_Cu],True,False)
|
|
s.obstacles.append(('RF_NEIGHBOUR_CLEARANCE',(232,175,258,184),pcb.F_Cu))
|
|
# The module itself extends into the copper-only antenna area.
|
|
rf = s.obstacles.pop()
|
|
s.put('U20',(247,165),180,radius=0,reason='MCU at board edge; antenna toward free south-east corner')
|
|
s.obstacles.append(rf)
|
|
s.put('J22',(264.866,151),90,radius=0,reason='USB debug connector opens at right edge')
|
|
s.put('J1',(63,67),90,radius=0,reason='12V input at left edge; fuse and TVS directly inboard')
|
|
s.put('J26',(59,148),270,radius=0,reason='Intake fan cable entry at left edge')
|
|
s.put('J27',(260,99),90,radius=0,reason='Exhaust fan cable entry at right edge')
|
|
# Three upper and two lower cells follow connector destinations, not old coordinates.
|
|
for ch,origin in [(1,(81,91)),(2,(137,86)),(3,(213,89)),(4,(102,141)),(5,(179,141))]:
|
|
power_cell(s,ch,origin)
|
|
for ref,position,rot in [('F1',(74,65),0),('Q1',(82,65),90),('D1',(87,57),0),
|
|
('C2',(98,59),0),('C3',(108,59),0),('Rs6',(118,62),0),
|
|
('U13',(119,69),0),('U19',(142,62),0),('L2',(149,62),0),
|
|
('U30',(169,62),0),('L3',(177,62),0),('Q3',(189,58),90),
|
|
('U29',(190,67),0),('U23',(150,157),0),('U6',(135,170),0),
|
|
('U7',(209,169),0),('Q7',(74,153),0),('Q8',(79,160),0),
|
|
('Q6',(85,159),0),('Q9',(72,166),0),('Q10',(251,111),0)]:
|
|
s.put(ref,position,rot,radius=6,reason='Functional input / auxiliary supply / control / fan zone')
|
|
for i,ref in enumerate(('R9A','R9B','R9C','R9D','R9E')):
|
|
s.put(ref,(204+i*10,58),0,radius=2,reason='Series bus discharge resistors spaced along ventilated north edge')
|
|
assignments = {
|
|
'C1':('Q1',4),'DZ1':('Q1',4),'R1':('Q1',4),'C4':('Q1',5),'C5':('Rs6',1),'C6':('Rs6',1),
|
|
'C7':('U19',3),'C8':('U19',6),'C9':('L2',2),'C10':('L2',2),'C11':('U13',6),
|
|
'R2':('U19',4),'R3':('U19',4),'R4':('U29',1),'R5':('U29',1),'R6':('Q3',4),
|
|
'C731':('U30',3),'C732':('U30',3),'C733':('U30',6),'C734':('L3',2),'C735':('L3',2),
|
|
'R706':('U30',4),'R707':('U30',4),
|
|
'C601':('U20',2),'C602':('U20',2),'C603':('U20',3),'R601':('U20',3),
|
|
'R602':('J22','A5'),'R603':('J22','B5'),'R604':('U20',22),'R605':('U20',8),
|
|
'R606':('U23',9),'R607':('U23',10),'R608':('U23',1),
|
|
'C606':('U23',24),'C607':('U6',15),'C608':('U6',13),'C609':('U6',16),
|
|
'C610':('U7',15),'C611':('U7',13),'C612':('U7',16),'R614':('U6',16),
|
|
'R615':('U6',5),'R616':('U7',16),'R617':('U7',5),
|
|
'C738':('Q7',3),'D2':('Q7',3),'DZ3':('Q7',1),'R740':('Q7',1),'R741':('Q8',3),
|
|
'R742':('Q8',1),'R743':('Q8',1),'R733':('Q6',1),'R734':('Q6',1),
|
|
'R744':('J26',3),'R745':('Q9',1),'R746':('Q9',1),'R747':('Q9',3),
|
|
'R748':('J27',3),'R749':('Q10',1),'R750':('Q10',1),'R751':('Q10',3)}
|
|
for ref,(anchor,pin) in assignments.items():
|
|
s.near(ref,anchor,pin,radius=12,reason='Pin-local support circuitry')
|
|
s.put('RT601',(92,102),0,radius=8,reason='Thermistor near CH1 hot area; avoid direct copper heat spreading')
|
|
s.put('RT602',(220,99),0,radius=8,reason='Thermistor near CH3 hot area; avoid direct copper heat spreading')
|
|
for ref,pin in [('R618',39),('C613',39),('R619',38),('C614',38)]:
|
|
s.near(ref,'U20',pin,radius=10,reason='ADC thermistor filter at MCU input')
|
|
return s
|
|
|
|
|
|
def main():
|
|
OUT.mkdir(parents=True,exist_ok=True)
|
|
before = OUT / 'before'
|
|
manifest = OUT / 'placement.json'
|
|
previous = json.loads(manifest.read_text(encoding='utf-8')) if manifest.exists() else {}
|
|
for n in (1,2):
|
|
name = f'PhoneRail-Board{n}'
|
|
for ext in ('.kicad_pcb','.kicad_pro','.kicad_dru'):
|
|
path = ROOT/name/(name+ext)
|
|
expected = previous.get('generated_files',{}).get(str(path.relative_to(ROOT)))
|
|
if expected and digest(path) != expected:
|
|
raise RuntimeError(f'Refusing to overwrite subsequent edits: {path}')
|
|
before.mkdir(exist_ok=True)
|
|
backup = before/path.name
|
|
if not backup.exists():
|
|
shutil.copy2(path,backup)
|
|
upper = carrier()
|
|
lower_board = lower(upper)
|
|
reports = {}
|
|
for s in (lower_board,upper):
|
|
s.finish()
|
|
reports[s.name] = {'moved':len(s.placements),'placements':s.placements}
|
|
print(s.name,'placed',len(s.placements),flush=True)
|
|
for s in (lower_board,upper):
|
|
shutil.copy2(OUT/s.path.name,s.path)
|
|
reports['status'] = REVISION + '_UNROUTED_NOT_RELEASED'
|
|
reports['generated_files'] = {str(s.path.relative_to(ROOT)):digest(s.path) for s in (lower_board,upper)}
|
|
manifest.write_text(json.dumps(reports,indent=2)+'\n',encoding='utf-8')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|