文件

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