149 行
8.0 KiB
Python
149 行
8.0 KiB
Python
"""Route ten explicitly reviewed Buck local connections using native KiCad DRC."""
|
|
import hashlib
|
|
import json
|
|
import shutil
|
|
import subprocess
|
|
|
|
import pcbnew as pcb
|
|
|
|
from schematic_maintenance import ROOT
|
|
from verify_pcb_layout import routing
|
|
|
|
OUT = ROOT / 'out/local-routing-r1'
|
|
BOARD = ROOT / 'PhoneRail-Board1/PhoneRail-Board1.kicad_pcb'
|
|
CLI = 'D:/KiCad/bin/kicad-cli.exe'
|
|
|
|
|
|
def drc(candidate, name):
|
|
report = OUT / f'{name}.json'
|
|
result = subprocess.run([CLI, 'pcb', 'drc', '--format', 'json', '-o', str(report), str(candidate)],
|
|
capture_output=True, encoding='utf-8', errors='replace', timeout=90)
|
|
report.with_suffix('.log').write_text(result.stdout+result.stderr, encoding='utf-8')
|
|
assert result.returncode == 0 and report.exists(), result.stderr
|
|
return json.loads(report.read_text(encoding='utf-8'))
|
|
|
|
|
|
def unconnected_count(board):
|
|
board.BuildConnectivity()
|
|
board.GetConnectivity().RecalculateRatsnest()
|
|
return board.GetConnectivity().GetUnconnectedCount(False)
|
|
|
|
|
|
def main():
|
|
if (OUT / 'verification.json').exists():
|
|
raise RuntimeError('Routing checkpoint already exists; do not overwrite later copper')
|
|
OUT.mkdir(parents=True, exist_ok=True)
|
|
board = pcb.LoadBoard(str(BOARD))
|
|
assert not list(board.GetTracks()), 'This first local routing pass requires an unrouted board'
|
|
initial_count = unconnected_count(board)
|
|
start_hash = hashlib.sha256(BOARD.read_bytes()).hexdigest()
|
|
backup = OUT / 'before'
|
|
backup.mkdir(exist_ok=True)
|
|
shutil.copy2(BOARD, backup / BOARD.name)
|
|
contract = json.loads((ROOT / 'pcb-routing-rules.json').read_text(encoding='utf-8'))
|
|
candidate = BOARD.parent / 'PhoneRail-Board1-local-candidate.kicad_pcb'
|
|
temporary = [candidate, candidate.with_suffix('.kicad_prl')]
|
|
assert all(not p.exists() for p in temporary), 'Candidate files already exist'
|
|
for suffix in ('.kicad_pro', '.kicad_dru'):
|
|
dest = candidate.with_suffix(suffix)
|
|
assert not dest.exists(), dest
|
|
shutil.copy2(BOARD.with_suffix(suffix), dest)
|
|
temporary.append(dest)
|
|
parts = {f.GetReference(): f for f in board.GetFootprints()}
|
|
accepted, blocked, exceptions = [], [], []
|
|
try:
|
|
pcb.SaveBoard(str(candidate), board)
|
|
initial = drc(candidate, 'baseline-drc')
|
|
assert not initial.get('violations'), 'Baseline geometry is not clean'
|
|
for ch in range(1, 6):
|
|
for function, pin, ref, other_pin in [('frequency', '1', f'R{ch}03', '2'),
|
|
('feedback_top', '7', f'R{ch}01', '2')]:
|
|
source = next(p for p in parts[f'U{ch}'].Pads() if p.GetNumber() == pin)
|
|
target = next(p for p in parts[ref].Pads() if p.GetNumber() == other_pin)
|
|
assert source.GetNetCode() == target.GetNetCode() and source.GetNetCode() > 0
|
|
assert source.IsOnLayer(pcb.F_Cu) and target.IsOnLayer(pcb.F_Cu)
|
|
a, b = source.GetPosition(), target.GetPosition()
|
|
key = f'CH{ch}-{function}'
|
|
success = False
|
|
dx, dy = b.x-a.x, b.y-a.y
|
|
assert abs(dy) > abs(dx)
|
|
sx, sy = (1 if dx > 0 else -1), (1 if dy > 0 else -1)
|
|
for variant, escape in enumerate((.5, .7, .9)):
|
|
offset = pcb.FromMM(escape)
|
|
assert offset + abs(dx) < abs(dy)
|
|
c = pcb.VECTOR2I(a.x, a.y+sy*offset)
|
|
d = pcb.VECTOR2I(b.x, c.y+sy*abs(dx))
|
|
tracks = []
|
|
for start, end in ((a, c), (c, d), (d, b)):
|
|
if start == end:
|
|
continue
|
|
t = pcb.PCB_TRACK(board)
|
|
t.SetStart(start)
|
|
t.SetEnd(end)
|
|
t.SetLayer(pcb.F_Cu)
|
|
t.SetWidth(pcb.FromMM(.2))
|
|
t.SetNet(source.GetNet())
|
|
board.Add(t)
|
|
tracks.append(t)
|
|
ids = [t.m_Uuid.AsString() for t in tracks]
|
|
length = sum(pcb.ToMM(t.GetLength()) for t in tracks)
|
|
exception = function == 'frequency' and length <= 3
|
|
if exception:
|
|
contract['direction']['exceptions'].extend(ids)
|
|
direction = routing(board, contract, 1)
|
|
if not direction['errors']:
|
|
pcb.SaveBoard(str(candidate), board)
|
|
report = drc(candidate, f'{key}-v{variant}')
|
|
if not report.get('violations'):
|
|
accepted.append({'connection': key, 'from': [f'U{ch}', pin], 'to': [ref, other_pin],
|
|
'net': source.GetNetname(), 'width_mm': .2, 'layer': 'F.Cu',
|
|
'length_mm': round(sum(pcb.ToMM(t.GetLength()) for t in tracks), 5),
|
|
'track_uuids': [t.m_Uuid.AsString() for t in tracks]})
|
|
success = True
|
|
if exception:
|
|
exceptions.append({'connection': key, 'uuids': ids, 'length_mm': round(length, 5),
|
|
'reason': 'Reviewed local RT/CLK resistor link, 45-degree bend and no via; 2mm axial separation plus pad offset'})
|
|
print('ROUTED', key, flush=True)
|
|
break
|
|
for t in tracks:
|
|
board.Remove(t)
|
|
if exception:
|
|
contract['direction']['exceptions'] = [u for u in contract['direction']['exceptions'] if u not in ids]
|
|
if not success:
|
|
blocked.append(key)
|
|
board.GetTitleBlock().SetTitle('PhoneRail Board 1 - partial local routing')
|
|
board.GetTitleBlock().SetRevision('R3-LOCAL-ROUTING')
|
|
for drawing in board.GetDrawings():
|
|
if isinstance(drawing, pcb.PCB_TEXT) and drawing.GetText() == 'BOARD 1 R2 - PLACED / UNROUTED':
|
|
drawing.SetText('BOARD 1 R3 - PARTIAL LOCAL ROUTING')
|
|
pcb.SaveBoard(str(candidate), board)
|
|
final = drc(candidate, 'final-drc')
|
|
assert not final.get('violations') and not routing(board, contract, 1)['errors']
|
|
reloaded = pcb.LoadBoard(str(candidate))
|
|
final_count = unconnected_count(reloaded)
|
|
reduction = initial_count - final_count
|
|
assert reduction == len(accepted), (reduction, len(accepted))
|
|
assert hashlib.sha256(BOARD.read_bytes()).hexdigest() == start_hash, 'Source changed during candidate review'
|
|
assert accepted, 'No reviewed local connection could be routed'
|
|
shutil.copy2(candidate, BOARD)
|
|
contract['direction'].setdefault('exception_reviews', []).extend(exceptions)
|
|
(ROOT / 'pcb-routing-rules.json').write_text(json.dumps(contract, indent=2, ensure_ascii=False)+'\n', encoding='utf-8')
|
|
result = {'status': 'PARTIAL_LOCAL_ROUTING_NOT_RELEASED', 'accepted': accepted, 'blocked': blocked,
|
|
'native_drc_violations': 0, 'direction_errors': 0, 'reviewed_direction_exceptions': exceptions,
|
|
'unconnected_before': initial_count,
|
|
'unconnected_after': final_count,
|
|
'drc_report_unconnected_items': len(final['unconnected_items']),
|
|
'count_source': 'Native CONNECTIVITY_DATA, all nets including hidden ratsnest; DRC report list is incomplete',
|
|
'track_segments': len(list(board.GetTracks())), 'vias': 0,
|
|
'limits': 'Only frequency setting and feedback top resistor links; complete feedback return, Kelvin, power and reference copper remain open',
|
|
'board_sha256': hashlib.sha256(BOARD.read_bytes()).hexdigest()}
|
|
(OUT / 'verification.json').write_text(json.dumps(result, indent=2) + '\n')
|
|
print(json.dumps({k: v for k, v in result.items() if k != 'accepted'}))
|
|
finally:
|
|
for path in temporary:
|
|
path.unlink(missing_ok=True)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|