240 行
12 KiB
Python
240 行
12 KiB
Python
"""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()
|