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

126 行
6.8 KiB
Python

"""Route independent shunt sense branches and reviewed local INA/eFuse signals."""
import hashlib
import json
import shutil
import pcbnew as pcb
import route_local_r1 as base
from fix_input_connector import copper, snapshot
from route_local_r2 import paths
from schematic_maintenance import ROOT
from verify_pcb_layout import routing
OUT = ROOT / 'out/local-routing-r3'
BOARD = base.BOARD
def digest(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
def main():
assert not OUT.exists(), 'Existing attempt or checkpoint requires review'
OUT.mkdir(parents=True)
base.OUT = OUT
rules = ROOT / 'pcb-routing-rules.json'
hashes = {p: digest(p) for p in (BOARD, rules)}
backup = OUT / 'before'
backup.mkdir()
for p in hashes:
shutil.copy2(p, backup / p.name)
contract = json.loads(rules.read_text(encoding='utf-8'))
board = pcb.LoadBoard(str(BOARD))
original_parts, original_copper = snapshot(board), copper(board)
initial = base.unconnected_count(board)
parts = {f.GetReference(): f for f in board.GetFootprints()}
candidate = BOARD.with_name('PhoneRail-Board1-local-r3-candidate.kicad_pcb')
temporary = [candidate.with_suffix(s) for s in ('.kicad_pcb', '.kicad_pro', '.kicad_dru', '.kicad_prl')]
assert all(not p.exists() for p in temporary)
accepted, blocked, reviews = [], [], []
try:
for suffix in ('.kicad_pro', '.kicad_dru'):
shutil.copy2(BOARD.with_suffix(suffix), candidate.with_suffix(suffix))
pcb.SaveBoard(str(candidate), board)
assert not base.drc(candidate, 'baseline-drc')['violations']
for ch in range(1, 6):
links = [
('kelvin_positive', f'U{ch+7}', '10', f'Rs{ch}01', '2', 'y', .2),
('kelvin_negative', f'U{ch+7}', '9', f'Rs{ch}01', '1', 'y', .2),
('ina_bypass_supply', f'U{ch+7}', '6', f'C{ch}13', '1', 'x', .25),
('efuse_enable_bias', f'U{ch+13}', '14', f'R{ch}14', '1', 'x', .2),
]
for function, ref, pin, dest, dest_pin, axis, width in links:
source = next(p for p in parts[ref].Pads() if p.GetNumber() == pin)
target = next(p for p in parts[dest].Pads() if p.GetNumber() == dest_pin)
assert source.GetNetCode() > 0 and source.GetNetCode() == target.GetNetCode()
assert source.IsOnLayer(pcb.F_Cu) and target.IsOnLayer(pcb.F_Cu)
key = f'CH{ch}-{function}'
failures = []
for variant, points in enumerate(paths(source.GetPosition(), target.GetPosition(), axis)):
tracks = []
for a, b in zip(points, points[1:]):
if a == b:
continue
t = pcb.PCB_TRACK(board)
t.SetStart(a)
t.SetEnd(b)
t.SetLayer(pcb.F_Cu)
t.SetWidth(pcb.FromMM(width))
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)
assert length < 8, (key, length)
contract['direction']['exceptions'].extend(ids)
pcb.SaveBoard(str(candidate), board)
report = base.drc(candidate, f'{key}-v{variant}')
if not report['violations'] and not routing(board, contract, 1)['errors']:
accepted.append({'connection': key, 'from': [ref, pin], 'to': [dest, dest_pin],
'net': source.GetNetname(), 'width_mm': width, 'layer': 'F.Cu',
'length_mm': round(length, 5), 'track_uuids': ids,
'role': 'MEASUREMENT_BRANCH_NOT_LOAD_CURRENT' if function.startswith('kelvin') else 'LOCAL_SIGNAL_OR_BYPASS'})
reviews.append({'connection': key, 'uuids': ids, 'length_mm': round(length, 5),
'reason': 'Short same-face shunt-to-INA measurement or local bias/bypass link; no vias; power-current copper and ground remain pending'})
print('ROUTED', key, flush=True)
break
failures.append(sorted({v['type'] for v in report['violations']}))
for t in tracks:
board.Remove(t)
contract['direction']['exceptions'] = [u for u in contract['direction']['exceptions'] if u not in ids]
else:
blocked.append({'connection': key, 'candidate_violations': failures})
print('BLOCKED', key, flush=True)
assert accepted, 'No acceptable routes'
pcb.SaveBoard(str(candidate), board)
final = base.drc(candidate, 'final-drc')
reloaded = pcb.LoadBoard(str(candidate))
remaining = base.unconnected_count(reloaded)
assert initial - remaining == len(accepted), (initial, remaining, len(accepted))
assert snapshot(reloaded) == original_parts
final_copper = copper(reloaded)
assert all(final_copper[k] == v for k, v in original_copper.items())
assert not final['violations'] and not routing(reloaded, contract, 1)['errors']
assert all(digest(p) == h for p, h in hashes.items()), 'Source changed during review'
contract['direction'].setdefault('exception_reviews', []).extend(reviews)
contract.setdefault('measurement_branches', []).extend(v for v in accepted if v['role'] == 'MEASUREMENT_BRANCH_NOT_LOAD_CURRENT')
shutil.copy2(candidate, BOARD)
rules.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,
'unconnected_before': initial, 'unconnected_after': remaining,
'native_drc_violations': 0, 'direction_errors': 0,
'track_segments': len(list(reloaded.GetTracks())), 'vias': 0,
'original_tracks_preserved': len(original_copper), 'unchanged_footprints': len(original_parts),
'reviewed_direction_exceptions': reviews, 'board_sha256': digest(BOARD),
'limits': 'Sense branches do not carry load current. Current-entry geometry of the two-terminal shunt lands, ground returns and complete power routing remain pending'}
(OUT / 'verification.json').write_text(json.dumps(result, indent=2)+'\n', encoding='utf-8')
print('COMPLETE', len(accepted), 'connections;', remaining, 'remaining', flush=True)
finally:
for path in temporary:
path.unlink(missing_ok=True)
if __name__ == '__main__':
main()