"""Verify the Hub ECO and produce a complete, explicit disposition of remaining ERC.""" import csv import json import re import xml.etree.ElementTree as ET import pcbnew as pcb from check_hardware import Board from hub_strap_checks import REFS, STRAPS, verify_hub_straps from revise_hub_straps import OUT, NC, CLI, PCB from schematic_maintenance import child, children, parse, properties from unconnected_inventory import classify, inventory, write_inventory def snapshot(path, omit=()): root = ET.parse(path).getroot() parts = {c.attrib['ref']: ET.tostring(c) for c in root.findall('components/comp') if c.attrib['ref'] not in omit} nets = {} for net in root.findall('nets/net'): pins = {(p.attrib['ref'], p.attrib['pin']): dict(p.attrib) for p in net.findall('node') if p.attrib['ref'] not in omit} if pins: nets[frozenset(pins)] = pins return parts, nets def main(): from check_power_paths import main as verify_power_paths verify_power_paths() rows, unconnected, boards = [], [], {} after = OUT / 'verified' after.mkdir(exist_ok=True) for number in (1, 2): b = Board(number) b.compile(CLI, after) added = REFS if number == 2 else set() old_parts, old_nets = snapshot(OUT / 'before' / f'Board{number}.xml') new_parts, new_nets = snapshot(after / f'Board{number}.xml', added) assert old_parts == new_parts, 'Unrelated component metadata changed' assert old_nets.keys() == new_nets.keys(), 'Unapproved connectivity change' assert set(b.parts) - set(old_parts) == added expected_nc = {(ref, pin) for ref, pins in NC[number].items() for pin in pins} changed = set() for members, pins in old_nets.items(): for endpoint, attrs in pins.items(): expected = dict(attrs) if endpoint in expected_nc: assert len(members) == 1 and 'no_connect' not in attrs['pintype'] expected['pintype'] += '+no_connect' assert expected == new_nets[members][endpoint], endpoint if attrs != expected: changed.add(endpoint) assert changed == expected_nc if number == 2: def check(label, good): assert good, label verify_hub_straps(b, check) current_pcb = pcb.LoadBoard(str(PCB)) previous_pcb = pcb.LoadBoard(str(OUT / 'before' / PCB.name)) footprints = {f.GetReference(): f for f in current_pcb.GetFootprints()} changed_pins = {str(pin) for pin in STRAPS.values()} for old_fp in previous_pcb.GetFootprints(): ref = old_fp.GetReference() new_fp = footprints[ref] assert old_fp.GetPosition() == new_fp.GetPosition() and old_fp.GetLayer() == new_fp.GetLayer(), ref assert old_fp.GetOrientationDegrees() == new_fp.GetOrientationDegrees(), ref new_pads = {p.GetNumber(): p for p in new_fp.Pads()} for old_pad in old_fp.Pads(): new_pad = new_pads[old_pad.GetNumber()] assert old_pad.GetPosition() == new_pad.GetPosition(), ref if ref != 'U32' or old_pad.GetNumber() not in changed_pins: assert old_pad.GetNetname() == new_pad.GetNetname(), (ref, old_pad.GetNumber()) report = json.loads((after / f'Board{number}-erc.json').read_text()) before = json.loads((OUT / 'before' / f'Board{number}-erc.json').read_text()) for key in ('ignored_checks', 'included_severities'): assert report[key] == before[key] for sheet in report['sheets']: for violation in sheet['violations']: description = violation['items'][0]['description'] match = re.search(r'^Symbol (\S+) .*? (\S+) \[', description) assert match, description ref, pin = match.groups() kind = violation['type'] net = b.pins.get((ref, pin), '') if ref.startswith('#PWR'): path = b.path.parent / (sheet['path'].strip('/') + '.kicad_sch') instance = next(s for s in children(parse(path.read_text(encoding='utf-8')), 'symbol') if properties(s)['Reference'][2] == ref) net = properties(instance)['Value'][2] status, blocking = 'REVIEW_REQUIRED', True category, action = classify(number, ref, pin) evidence = '' if kind == 'power_pin_not_driven': category = 'supply_model' status, blocking = 'TEMPORARILY_ACCEPTED_FOR_ROUTING', False evidence = 'out/power-path-review/verification.json' action = 'Topology traced; retain ERC until source operation/ratings are reviewed, then add scoped PWR_FLAG' elif category in ('hub_port_power', 'hub_overcurrent'): category = 'hub_power_architecture' evidence = '../五路直供电源板设计.md sections 15.2,21.6; USB2517.pdf pages 10,12' action = 'Intentional external MCU control; verify 5V short-circuit protection and USB port power/OC reporting before release' elif category == 'unused_dac_channel': evidence = 'DAC63204.pdf pin table and power-down/NVM sections; ../firmware/components/drv/dac63204.c' action = 'Runtime keeps unused channels Hi-Z; verify manufactured NVM image and recall/fault state before NC' elif category == 'mcu_recovery': action = 'Add accessible IO0/UART recovery test points; current schematic explicitly says they are not fitted' rows.append({'board': number, 'sheet': sheet['path'], 'reference': ref, 'pin': pin, 'erc_type': kind, 'severity': violation['severity'], 'net': net, 'category': category, 'disposition': status, 'temporary_routing_acceptance': not blocking, 'release_blocking': True, 'evidence': evidence, 'next_action': action}) unconnected.extend(inventory(b)) boards[number] = {'original_components_unchanged': len(old_parts), 'added_components': sorted(added), 'original_endpoint_groups_preserved': len(new_nets), 'approved_nc': sorted(changed), 'erc': b.erc} with (OUT / 'erc-disposition.csv').open('w', encoding='utf-8-sig', newline='') as f: writer = csv.DictWriter(f, fieldnames=list(rows[0])) writer.writeheader() writer.writerows(rows) (OUT / 'erc-disposition.json').write_text(json.dumps(rows, ensure_ascii=False, indent=2) + '\n', encoding='utf-8') result = {'status': 'ECO_VERIFIED_NOT_RELEASED', 'boards': boards, 'remaining_erc': len(rows), 'temporary_routing_acceptances': sum(r['temporary_routing_acceptance'] for r in rows), 'release_review_required': sum(r['release_blocking'] for r in rows), 'unconnected': write_inventory(OUT, unconnected)} (OUT / 'verification.json').write_text(json.dumps(result, indent=2) + '\n') print(json.dumps(result)) if __name__ == '__main__': main()