130 行
7.3 KiB
Python
130 行
7.3 KiB
Python
"""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()
|