"""Review IC pin types; --apply performs a guarded, mechanical type-only update.""" import argparse from collections import Counter import hashlib import json from pathlib import Path import re import xml.etree.ElementTree as ET from schematic_maintenance import ROOT, child, children, parse, properties LIB = Path('D:/KiCad/share/kicad/symbols') TARGETS = { 'INA226AIDGSR': ('Sensor_Energy', 'INA226'), 'LM75BD,118': ('Sensor_Temperature', 'LM75B'), 'PCA9555PW,112': ('Interface_Expansion', 'PCA9555PW'), 'ESP32-S3-WROOM-1': ('RF_Module', 'ESP32-S3-WROOM-1'), 'LMR51430YDDCR': ('Regulator_Switching', 'LMR51430'), 'USBLC6-2SC6_C2687116': ('Power_Protection', 'USBLC6-2SC6'), } OUT = ROOT / 'out/ic-pin-review' # Exact name differences checked during review, indexed by physical pin number. ALIASES = { 'INA226AIDGSR': {'3': ('Alert', '~{Alert}'), '6': ('VS+', 'VS'), '8': ('VBUS', 'Vbus'), '9': ('VIN-', 'Vin-'), '10': ('VIN+', 'Vin+')}, 'LM75BD,118': {'3': ('OS', 'O.S.'), '8': ('VCC', '+Vs')}, 'PCA9555PW,112': {'1': ('INT#', '~{INT}')}, 'ESP32-S3-WROOM-1': {'13': ('IO19', 'USB_D-'), '14': ('IO20', 'USB_D+'), '41': ('EPAD', 'GND')}, } def digest(source): return hashlib.sha256(source.encode('utf-8')).hexdigest() def pins(symbol): return {child(p, 'number')[1]: (child(p, 'name')[1], p[1]) for unit in children(symbol, 'symbol') for p in children(unit, 'pin')} def reference_pins(library, name): source = (LIB / (library + '.kicad_sym')).read_text(encoding='utf-8') symbols = {s[1]: s for s in children(parse(source), 'symbol')} symbol = symbols[name] while child(symbol, 'extends'): symbol = symbols[child(symbol, 'extends')[1]] return pins(symbol) def verify_netlists(standards): from check_hardware import Board from manufacturer_ic_pins import check_common_pins, check_hub_supply from unconnected_inventory import inventory, write_inventory after = OUT / 'after' after.mkdir(exist_ok=True) result = {'status': 'PASS', 'boards': {}} queue = [] unconnected = [] for number in (1, 2): board = Board(number) board.compile('D:/KiCad/bin/kicad-cli.exe', after) unconnected.extend(inventory(board)) old = ET.parse(OUT / 'before' / f'Board{number}.xml').getroot() new = ET.parse(after / f'Board{number}.xml').getroot() components = lambda root: {c.attrib['ref']: ET.tostring(c) for c in root.findall('components/comp')} old_components, new_components = components(old), components(new) changed_components = [ref for ref in old_components.keys() | new_components.keys() if old_components.get(ref) != new_components.get(ref)] if changed_components: from replace_c737_tantalum import approved_metadata_transition if number != 2 or changed_components != ['C737'] or not approved_metadata_transition( old_components['C737'], new_components['C737']): raise ValueError(f'Board{number}: unapproved component metadata change') def nets(root): return {n.attrib['name']: { (p.attrib['ref'], p.attrib['pin']): dict(p.attrib) for p in n.findall('node')} for n in root.findall('nets/net')} old_nets, new_nets = nets(old), nets(new) if set(old_nets) != set(new_nets): raise ValueError(f'Board{number}: network names changed') types = {c.attrib['ref']: c.find('libsource').attrib['part'] for c in new.findall('components/comp')} changed = 0 for net, old_pins in old_nets.items(): new_pins = new_nets[net] if set(old_pins) != set(new_pins): raise ValueError(f'Board{number}: {net} endpoints changed') for endpoint, old_attrs in old_pins.items(): new_attrs = new_pins[endpoint] expected = dict(old_attrs) name = types[endpoint[0]] if name in standards: expected['pintype'] = standards[name][endpoint[1]][1] if '+no_connect' in old_attrs.get('pintype', ''): expected['pintype'] += '+no_connect' if expected != new_attrs: raise ValueError(f'Board{number}: unapproved pin change: {endpoint}') changed += old_attrs != new_attrs old_erc = json.loads((OUT / 'before' / f'Board{number}-erc.json').read_text(encoding='utf-8')) new_erc = json.loads((after / f'Board{number}-erc.json').read_text(encoding='utf-8')) if old_erc['ignored_checks'] != new_erc['ignored_checks'] or old_erc['included_severities'] != new_erc['included_severities']: raise ValueError('ERC check configuration changed') erc_counts = lambda report: dict(Counter(v['type'] for s in report['sheets'] for v in s['violations'])) for sheet in new_erc['sheets']: for violation in sheet['violations']: queue.append({'board': number, 'sheet': sheet['path'], **violation}) result['boards'][board.name] = { 'components_unchanged': len(types) - len(changed_components), 'approved_component_metadata_changes': changed_components, 'net_names_and_endpoints_unchanged': len(new_nets), 'approved_instance_pin_type_changes': changed, 'erc_before': erc_counts(old_erc), 'erc_after': erc_counts(new_erc), 'erc_configuration_unchanged': True, 'common_pad_checks': check_common_pins(board), 'hub_supply_checks': check_hub_supply(board), } result['unconnected_inventory'] = write_inventory(OUT, unconnected) (OUT / 'verification.json').write_text(json.dumps(result, indent=2) + '\n', encoding='utf-8') (OUT / 'remaining-erc.json').write_text(json.dumps(queue, indent=2, ensure_ascii=False) + '\n', encoding='utf-8') print(json.dumps({'status': result['status'], 'boards': { name: {k: len(v) if k.endswith('_checks') else v for k, v in entry.items()} for name, entry in result['boards'].items()}})) def main(): global OUT ap = argparse.ArgumentParser(description=__doc__) ap.add_argument('--apply', action='store_true') ap.add_argument('--verify-netlist', action='store_true', help='Recompile and compare against the saved pre-change baseline') ap.add_argument('--batch', choices=('standard', 'manufacturer', 'hub', 'discrete'), default='standard') args = ap.parse_args() if args.batch != 'standard': from manufacturer_ic_pins import TABLES, HUB_TABLES, DISCRETE_TABLES, references standards = {'manufacturer': TABLES, 'hub': HUB_TABLES, 'discrete': DISCRETE_TABLES}[args.batch] evidence = references(hub=args.batch == 'hub', discrete=args.batch == 'discrete') OUT = ROOT / 'out' / {'manufacturer': 'ic-pin-review-r2', 'hub': 'ic-pin-review-hub', 'discrete': 'ic-pin-review-discrete'}[args.batch] else: standards = {name: reference_pins(*spec) for name, spec in TARGETS.items()} evidence = {n: {'library': spec[0], 'symbol': spec[1], 'library_sha256': digest((LIB / (spec[0] + '.kicad_sym')).read_text(encoding='utf-8')), 'pins': standards[n]} for n, spec in TARGETS.items()} paths = [ROOT / 'lib/PhoneRail.kicad_sym', *sorted(ROOT.glob('PhoneRail-Board*/*.kicad_sch'))] pending = [] rows = [] instances = Counter() for path in paths: source = path.read_text(encoding='utf-8') root = parse(source) if root[0] == 'kicad_sch': for instance in children(root, 'symbol'): name = child(instance, 'lib_id')[1].removeprefix('PhoneRail:') if name in standards: instances[name] += 1 symbols = children(child(root, 'lib_symbols'), 'symbol') else: symbols = children(root, 'symbol') edits = [] for symbol in symbols: name = symbol[1].removeprefix('PhoneRail:') if name not in standards: continue standard = standards[name] local = pins(symbol) if set(local) != set(standard): raise ValueError(f'{path}: {name} pin-number mismatch') for unit in children(symbol, 'symbol'): for pin in children(unit, 'pin'): number = child(pin, 'number')[1] actual_name, actual_type = local[number] expected_name, expected_type = standard[number] if actual_name != expected_name and ALIASES.get(name, {}).get(number) != (actual_name, expected_name): raise ValueError(f'{path}: {name}.{number} unreviewed name difference') if actual_type not in ('unspecified', expected_type): raise ValueError(f'{path}: {name}.{number} unexpected existing type {actual_type}') rows.append({'file': str(path.relative_to(ROOT)), 'symbol': name, 'pin': number, 'name': actual_name, 'expected_name': expected_name, 'actual_type': actual_type, 'expected_type': expected_type}) if actual_type != expected_type: raw = source[pin.start:pin.end] edits.append((pin.start, pin.end, re.sub(r'^\(pin\s+\S+', '(pin ' + expected_type, raw))) updated = source for start, end, replacement in sorted(edits, reverse=True): updated = updated[:start] + replacement + updated[end:] if edits: # Prove the complete document differs only in electrical-type tokens. strip = lambda text: re.sub(r'\(pin\s+(?:unspecified|power_in|power_out|input|output|passive|bidirectional|open_collector)\s', '(pin TYPE ', text) if strip(updated) != strip(source): raise ValueError(f'Non-type change in {path}') pending.append((path, source, updated, len(edits))) if not all(instances[name] or name == 'TPS22918DBVR' for name in standards): raise ValueError('Some reviewed symbols have no instances') OUT.mkdir(parents=True, exist_ok=True) if args.apply and pending: from check_hardware import Board before = OUT / 'before' if before.exists(): raise RuntimeError('Review baseline already exists; inspect changes before another apply') before.mkdir() for number in (1, 2): Board(number).compile('D:/KiCad/bin/kicad-cli.exe', before) for path, source, _, _ in pending: backup = before / path.relative_to(ROOT) backup.parent.mkdir(parents=True, exist_ok=True) backup.write_text(source, encoding='utf-8') for path, source, _, _ in pending: if path.read_text(encoding='utf-8') != source: raise RuntimeError(f'Concurrent edit: {path}') for path, _, updated, _ in pending: path.write_text(updated, encoding='utf-8') (OUT / 'applied.json').write_text(json.dumps({ 'pin_declarations_changed': sum(p[3] for p in pending), 'files': [{'file': str(p.relative_to(ROOT)), 'before_sha256': digest(s), 'after_sha256': digest(u), 'changed_pins': n} for p, s, u, n in pending], 'pins': rows, }, indent=2) + '\n', encoding='utf-8') report = {'status': 'APPLIED_REQUIRES_ERC_AND_PARITY' if args.apply and pending else 'PASS' if not pending else 'TYPE_UPDATES_PENDING', 'instances': dict(instances), 'instance_pins': sum(instances[n] * len(standards[n]) for n in standards), 'declarations_checked': len(rows), 'pending_updates': sum(p[3] for p in pending), 'references': evidence, 'pins': rows} (OUT / 'pin-types.json').write_text(json.dumps(report, indent=2) + '\n', encoding='utf-8') print(json.dumps({k: v for k, v in report.items() if k not in ('pins', 'references')})) if pending and not args.apply: raise SystemExit(1) if args.verify_netlist: from manufacturer_ic_pins import TABLES, HUB_TABLES, DISCRETE_TABLES, references references() references(hub=True) references(discrete=True) standards = {**{n: reference_pins(*spec) for n, spec in TARGETS.items()}, **TABLES, **HUB_TABLES, **DISCRETE_TABLES} verify_netlists(standards) if __name__ == '__main__': main()