文件
phonerail/kicad/scripts/route_local_r1.py
T

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