"""Guarded Hub strap pulldown ECO and reviewed USB/LED NC markers.""" import argparse import hashlib import json import math import shutil import uuid import xml.etree.ElementTree as ET import pcbnew as pcb from bootstrap_pcb import native_net_names, xy from check_hardware import Board from optimize_pcb_layout import Layout, box_mm, expand, point, place_silkscreen from schematic_maintenance import ROOT, child, children, parse, properties from hub_strap_checks import STRAPS OUT = ROOT / 'out/hub-straps-r1' CLI = 'D:/KiCad/bin/kicad-cli.exe' NC = {1: {'J22': ['A8', 'B8', 'A4B9', 'B4A9']}, 2: {'J23': ['A8', 'B8'], 'U32': ['14', '16', '18', '32']}} SHEETS = [ROOT / 'PhoneRail-Board1/P3-CONTROL.kicad_sch', ROOT / 'PhoneRail-Board2/C2-HUB.kicad_sch'] PCB = ROOT / 'PhoneRail-Board2/PhoneRail-Board2.kicad_pcb' def uid(key): return str(uuid.uuid5(uuid.NAMESPACE_URL, 'PhoneRail/hub-straps-r1/' + key)) def pin_position(root, instance, number): assert child(instance, 'mirror') is None, 'Mirror requires explicit geometry review' lib = next(s for s in children(child(root, 'lib_symbols'), 'symbol') if s[1] == child(instance, 'lib_id')[1]) pin = next(p for u in children(lib, 'symbol') for p in children(u, 'pin') if child(p, 'number')[1] == str(number)) x, y, angle = map(float, child(instance, 'at')[1:]) px, py = map(float, child(pin, 'at')[1:3]) a = math.radians(angle) return round(x + px*math.cos(a)-py*math.sin(a), 5), round(y-px*math.sin(a)-py*math.cos(a), 5) def label(name, x, y, key, global_label=False, angle=0): kind = 'global_label' if global_label else 'label' shape = '(shape input)' if global_label else '' return (f'({kind} "{name}" {shape} (at {x} {y} {angle}) ' f'(effects (font (size 1 1)) (justify {"left" if angle == 0 else "right"} bottom)) (uuid "{uid(key)}"))') def layout_notes(source): root = parse(source) for note in sorted(children(root, 'text'), key=lambda n: n.start, reverse=True): source = source[:note.start] + source[note.end:] notes = [ 'CONFIG: CFG_SEL[2:0] = 000; internal defaults + straps.', 'ROM 06h = 9Bh: self-powered operation.', 'PRTPWR / OCS open by existing architecture.', '5V short-circuit / port OC response: REVIEW OPEN.', 'R900-R909: 100k explicit strap-low (Fig 7-3).', 'PRT_SWP[7:1] = 0: normal D+/D- polarity.', 'BOOST[1:0] = 00; GANG_EN = 0.', 'Ports 6/7: R714/R715 10k to +3V3_HUB.', 'Port-disable / enumeration: verify on prototype.', 'RBIAS: 12.0k 1%; Y1: 24MHz.', 'VDD18/VDD18PLL: >=1uF, ESR <0.1 ohm.', 'VBUS_DET: R709/R710 = 100k/100k.', 'UP_VBUS_DET: R721/R722 = 10k/20k.', 'NC: USB2-only SBU and four unused LED outputs.', ] additions = [] for index, text in enumerate(notes): additions.append(f'(text "{text}" (at 310 {45+index*7} 0) ' '(effects (font (size 1.27 1.27)) (justify left bottom)) ' f'(uuid "{uid("review-note-"+str(index))}"))') additions.append('(text "R900-R909: USB2517 strap-low pulldowns, 100k each" (at 25.4 216 0) ' '(effects (font (size 1.27 1.27)) (justify left bottom)) ' f'(uuid "{uid("bank-note")}"))') root = parse(source) return source[:root.end-1] + '\n'.join(additions) + '\n' + source[root.end-1:] def edit_sheets(): for number, path in enumerate(SHEETS, 1): source = path.read_text(encoding='utf-8') root = parse(source) instances = {properties(s)['Reference'][2]: s for s in children(root, 'symbol')} additions = [] for ref, pins in NC[number].items(): for pin in pins: x, y = pin_position(root, instances[ref], pin) additions.append(f'(no_connect (at {x} {y}) (uuid "{uid(ref+pin)}"))') if number == 2: template_source = (ROOT / 'PhoneRail-Board2/C4-SITE1.kicad_sch').read_text(encoding='utf-8') template_root = parse(template_source) template = next(s for s in children(template_root, 'symbol') if properties(s)['Reference'][2] == 'R741') props = {k: p[2] for k, p in properties(template).items()} lib_id = child(template, 'lib_id')[1] library = child(root, 'lib_symbols') if not any(s[1] == lib_id for s in children(library, 'symbol')): lib = next(s for s in children(child(template_root, 'lib_symbols'), 'symbol') if s[1] == lib_id) source = source[:library.end-1] + template_source[lib.start:lib.end] + '\n' + source[library.end-1:] root = parse(source) sheet_path = child(child(child(instances['R711'], 'instances'), 'project'), 'path')[1] for index, (refnum, pin) in enumerate(STRAPS.items()): ref, name = f'R{refnum}', f'STRAP_{pin}' x, y = 25.4 + index*30.48, 236.22 fields = [] for key, value in {**props, 'Reference': ref}.items(): visible = key in ('Reference', 'Value') fx, fy = (x+2.54, y+(2.54 if key == 'Value' else 0)) if visible else (x, y) fields.append(f'(property {json.dumps(key)} {json.dumps(value, ensure_ascii=False)} ' f'(at {fx} {fy} 270) (effects (font (size 1.27 1.27)) ' f'(justify left bottom) {"" if visible else "(hide yes)"}))') additions.append(f'(symbol (lib_id "{lib_id}") (at {x} {y} 90) (unit 1) ' '(exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no) ' f'(uuid "{uid(ref)}") ' + ' '.join(fields) + f' (instances (project "PhoneRail-Board2" (path "{sheet_path}" ' f'(reference "{ref}") (unit 1)))))') # The existing resistor symbol has pins at local x = +/-5.08. new_instance = parse(additions[-1]) rx1, ry1 = pin_position(root, new_instance, 1) rx2, ry2 = pin_position(root, new_instance, 2) additions += [label('GND', rx1, ry1, ref+'gnd', True), label(name, rx2, ry2, ref+'net')] hx, hy = pin_position(root, instances['U32'], pin) additions.append(label(name, hx, hy, 'hub'+str(pin), angle=180 if hx < 128 else 0)) additions.append('(text "R900-R909: 100k strap-low; USB2517 DS00001598C Fig 7-3." ' '(at 25.4 216 0) (effects (font (size 1.27 1.27)) (justify left bottom)) ' f'(uuid "{uid("note")}"))') source = source[:root.end-1] + '\n' + '\n'.join(additions) + '\n' + source[root.end-1:] if number == 2: source = layout_notes(source) path.write_text(source, encoding='utf-8') def sync_pcb(source): board = pcb.LoadBoard(str(PCB)) names = native_net_names(source) nets = {n.GetNetname(): n for n in board.GetNetsByName().values()} for name in names.values(): if name not in nets: net = pcb.NETINFO_ITEM(board, name) board.Add(net) nets[name] = net existing = {f.GetReference(): f for f in board.GetFootprints()} components = {c.attrib['ref']: c for c in ET.parse(OUT / 'after/Board2.xml').findall('components/comp')} root_uuid = child(parse(source.path.read_text(encoding='utf-8')), 'uuid')[1] layout = Layout.__new__(Layout) layout.number, layout.board, layout.parts = 2, board, existing layout.done, layout.placements = set(existing), {} layout.obstacles = [(r, expand(box_mm(f), .35), -1 if any(p.GetAttribute() in (pcb.PAD_ATTRIB_PTH, pcb.PAD_ATTRIB_NPTH) for p in f.Pads()) else f.GetLayer()) for r, f in existing.items()] for z in board.Zones(): if z.GetIsRuleArea() and z.GetDoNotAllowFootprints(): layout.obstacles.append((z.GetZoneName(), expand(box_mm(z), .25), -1 if z.GetLayerSet().Contains(pcb.F_Cu) else pcb.B_Cu)) center = point(existing['U32']) for refnum, pin in STRAPS.items(): ref = f'R{refnum}' c = components[ref] fp = pcb.FootprintLoad(str(ROOT / 'lib/PhoneRail.pretty'), 'R0402') fp.SetFPIDAsString('PhoneRail:R0402') fp.SetReference(ref) fp.SetValue(c.findtext('value')) fp.SetSheetname('C2-HUB') fp.SetSheetfile('C2-HUB.kicad_sch') for field in c.findall('fields/field'): if field.get('name') not in {'Reference', 'Value', 'Footprint'}: fp.SetField(field.get('name'), field.text or '') path = pcb.KIID_PATH() for key in [root_uuid, *c.find('sheetpath').get('tstamps').strip('/').split('/'), c.findtext('tstamps')]: path.push_back(pcb.KIID(key)) fp.SetPath(path) board.Add(fp) existing[ref] = fp pad = next(p for p in existing['U32'].Pads() if p.GetNumber() == str(pin)) px, py = point(pad) dx, dy = px-center[0], py-center[1] norm = math.hypot(dx, dy) layout.put(ref, (px+dx/norm*2, py+dy/norm*2), rotation=(0,90,180,270), radius=12, reason=f'100k pull-down for U32.{pin}; DS00001598C Fig 7-3') fp.Reference().SetLayer(pcb.B_Fab) fp.SetField('ReferenceOnFabOnly', 'yes') for field in fp.GetFields(): field.SetVisible(field.GetName() == 'Reference') field.SetMirrored(True) for ref, fp in existing.items(): for pad in fp.Pads(): endpoint = (ref, pad.GetNumber()) if endpoint in source.pins: pad.SetNet(nets[names[source.pins[endpoint]]]) place_silkscreen(board, {'C745', 'C747', 'C748', 'R709', 'R714', 'R719'}) pcb.SaveBoard(str(PCB), board) (OUT / 'placement.json').write_text(json.dumps(layout.placements, indent=2) + '\n') def main(): ap = argparse.ArgumentParser(description=__doc__) ap.add_argument('--apply', action='store_true') args = ap.parse_args() if not args.apply: raise SystemExit('Use --apply once; validate with verify_erc_disposition.py afterwards') before = OUT / 'before' if before.exists(): raise RuntimeError('Existing ECO baseline; refusing to repeat mutations') before.mkdir(parents=True) for path in [*SHEETS, PCB]: shutil.copy2(path, before / path.name) for number in (1, 2): Board(number).compile(CLI, before) edit_sheets() after = OUT / 'after' after.mkdir() for number in (1, 2): board = Board(number) board.compile(CLI, after) if number == 2: sync_pcb(board) hashes = {str(p.relative_to(ROOT)): hashlib.sha256(p.read_bytes()).hexdigest() for p in [*SHEETS, PCB]} (OUT / 'applied.json').write_text(json.dumps(hashes, indent=2) + '\n') if __name__ == '__main__': main()