"""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()