kicad:EasyEDA→KiCad 工程(Board1/Board2)、库、脚本与评审文档(IC 引脚、风扇、热、EDL 供电、布局规则)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
@@ -0,0 +1,224 @@
|
||||
"""Apply the reviewed C737 solid-tantalum substitution, preserving pin 2 as positive."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import pcbnew as pcb
|
||||
|
||||
from check_hardware import Board
|
||||
from schematic_maintenance import ROOT, child, children, parse, properties
|
||||
|
||||
OUT = ROOT / 'out/c737-tantalum'
|
||||
SHEET = ROOT / 'PhoneRail-Board2/C1-CONN.kicad_sch'
|
||||
BOARD = ROOT / 'PhoneRail-Board2/PhoneRail-Board2.kicad_pcb'
|
||||
LIB = ROOT / 'lib/PhoneRail.kicad_sym'
|
||||
FP_TABLE = ROOT / 'PhoneRail-Board2/fp-lib-table'
|
||||
FP_DIR = ROOT / 'lib/PhoneRail-Qualified.pretty'
|
||||
FP_NAME = 'CP_EIA-3216-18_Kemet-A_Pos2'
|
||||
SYMBOL = 'T491A226K010AT_Pos2'
|
||||
META = {'Value': '22uF / 10V Ta', 'Footprint': 'PhoneRail-Qualified:' + FP_NAME,
|
||||
'Datasheet': 'https://item.szlcsc.com/datasheet/T491A226K010AT/117829.html',
|
||||
'Description': '22uF +/-10% 10V solid tantalum MnO2; pin 2 positive, pin 1 negative; KEMET A 3216-18',
|
||||
'Manufacturer': 'KEMET', 'MPN': 'T491A226K010AT', 'LCSC': 'C116575'}
|
||||
CLI = 'D:/KiCad/bin/kicad-cli.exe'
|
||||
|
||||
|
||||
def update_spans(source, edits):
|
||||
for start, end, text in sorted(edits, reverse=True):
|
||||
source = source[:start] + text + source[end:]
|
||||
return source
|
||||
|
||||
|
||||
def net_signature(root):
|
||||
return {n.attrib['name']: {(p.attrib['ref'], p.attrib['pin']) for p in n.findall('node')}
|
||||
for n in root.findall('nets/net')}
|
||||
|
||||
|
||||
def approved_metadata_transition(before, after):
|
||||
old, new = ET.fromstring(before), ET.fromstring(after)
|
||||
if old.attrib.get('ref') != 'C737' or old.findtext('footprint') != 'PhoneRail:C1206':
|
||||
return False
|
||||
if old.find('libsource').attrib.get('part') != 'CL31A226KAHNNNE':
|
||||
return False
|
||||
for tag, key in (('value', 'Value'), ('footprint', 'Footprint'), ('datasheet', 'Datasheet'), ('description', 'Description')):
|
||||
old.find(tag).text = META[key]
|
||||
for field in old.findall('fields/field'):
|
||||
if field.attrib['name'] in META:
|
||||
field.text = META[field.attrib['name']]
|
||||
for prop in old.findall('property'):
|
||||
if prop.attrib['name'] in META:
|
||||
prop.attrib['value'] = META[prop.attrib['name']]
|
||||
old.find('libsource').attrib['part'] = SYMBOL
|
||||
return ET.tostring(old) == ET.tostring(new)
|
||||
|
||||
|
||||
def field_style(footprint, template):
|
||||
for field in footprint.GetFields():
|
||||
if field.GetName() not in ('Reference', 'Value'):
|
||||
field.SetVisible(False)
|
||||
field.SetMirrored(footprint.IsFlipped())
|
||||
for current, previous in ((footprint.Reference(), template.Reference()), (footprint.Value(), template.Value())):
|
||||
current.SetPosition(previous.GetPosition())
|
||||
current.SetTextAngle(previous.GetTextAngle())
|
||||
current.SetTextSize(previous.GetTextSize())
|
||||
current.SetVisible(previous.IsVisible())
|
||||
current.SetMirrored(previous.IsMirrored())
|
||||
|
||||
|
||||
def verify():
|
||||
after = OUT / 'after'
|
||||
after.mkdir(exist_ok=True)
|
||||
b = Board(2)
|
||||
b.compile(CLI, after)
|
||||
assert all(b.parts['C737'][k] == v for k, v in META.items())
|
||||
assert b.net('C737', 1) == 'GND' and b.net('C737', 2) == '+3V3_HUB'
|
||||
old = ET.parse(OUT / 'before/Board2.xml').getroot()
|
||||
new = ET.parse(after / 'Board2.xml').getroot()
|
||||
assert net_signature(old) == net_signature(new), 'Unexpected connectivity change'
|
||||
comps = lambda r: {c.attrib['ref']: ET.tostring(c) for c in r.findall('components/comp') if c.attrib['ref'] != 'C737'}
|
||||
assert comps(old) == comps(new), 'Unrelated component metadata changed'
|
||||
board = pcb.LoadBoard(str(BOARD))
|
||||
before = pcb.LoadBoard(str(OUT / 'before' / BOARD.name))
|
||||
fps = {f.GetReference(): f for f in board.GetFootprints()}
|
||||
for f in before.GetFootprints():
|
||||
if f.GetReference() == 'C737':
|
||||
continue
|
||||
current = fps[f.GetReference()]
|
||||
assert current.GetPosition() == f.GetPosition() and current.GetLayer() == f.GetLayer()
|
||||
assert current.GetOrientationDegrees() == f.GetOrientationDegrees()
|
||||
assert current.GetFPIDAsString() == f.GetFPIDAsString()
|
||||
assert [(p.GetNumber(), p.GetPosition(), p.GetNetname()) for p in current.Pads()] == [
|
||||
(p.GetNumber(), p.GetPosition(), p.GetNetname()) for p in f.Pads()]
|
||||
f = fps['C737']
|
||||
assert f.GetFPIDAsString() == META['Footprint'] and f.GetValue() == META['Value']
|
||||
pads = {p.GetNumber(): p for p in f.Pads()}
|
||||
assert pads['2'].GetNetname() == '+3V3_HUB' and pads['1'].GetNetname() == 'GND'
|
||||
assert f.GetLayer() == pcb.B_Cu and pads['2'].GetPosition().y < pads['1'].GetPosition().y
|
||||
assert len(list(board.GetTracks())) == len(list(before.GetTracks()))
|
||||
drc_path = OUT / 'Board2-drc.json'
|
||||
drc_path.unlink(missing_ok=True)
|
||||
result = subprocess.run([CLI, 'pcb', 'drc', '--format', 'json', '--schematic-parity',
|
||||
'-o', str(drc_path), str(BOARD)], capture_output=True,
|
||||
encoding='utf-8', errors='replace', timeout=180)
|
||||
drc_path.with_suffix('.log').write_text(result.stdout + result.stderr, encoding='utf-8')
|
||||
if result.returncode or not drc_path.exists():
|
||||
raise RuntimeError('Native DRC failed to run')
|
||||
drc = json.loads(drc_path.read_text(encoding='utf-8'))
|
||||
assert not drc.get('violations') and not drc.get('schematic_parity'), 'Native DRC or parity failed'
|
||||
report = {'status': 'SUBSTITUTION_VERIFIED_NOT_PRODUCTION_RELEASE', 'reference': 'C737', **META,
|
||||
'positive_pin': '2', 'positive_net': '+3V3_HUB', 'negative_pin': '1', 'negative_net': 'GND',
|
||||
'unchanged_networks': len(net_signature(new)), 'unrelated_components_unchanged': len(comps(new)),
|
||||
'position_mm': [pcb.ToMM(f.GetPosition().x), pcb.ToMM(f.GetPosition().y)],
|
||||
'orientation_degrees': f.GetOrientationDegrees(), 'erc': b.erc,
|
||||
'body_max_height_mm': 1.8, 'voltage_rating_v': 10, 'nominal_rail_v': 3.3,
|
||||
'native_geometry_drc': 0, 'native_schematic_parity': 0,
|
||||
'unconnected_pcb_items': len(drc.get('unconnected_items', []))}
|
||||
(OUT / 'verification.json').write_text(json.dumps(report, indent=2) + '\n', encoding='utf-8')
|
||||
print(json.dumps(report))
|
||||
|
||||
|
||||
def apply():
|
||||
backup = OUT / 'before'
|
||||
if backup.exists():
|
||||
for path in (SHEET, BOARD, LIB, FP_TABLE):
|
||||
if path.read_bytes() != (backup / path.name).read_bytes():
|
||||
raise RuntimeError('C737 baseline differs; use verification, do not overwrite later work')
|
||||
pdf = ROOT / 'datasheets/ic/KEMET-T491.pdf'
|
||||
assert hashlib.sha256(pdf.read_bytes()).hexdigest() == '47bd290c07415015937bb42a5998eb0c6ccf5760c539ab9d180233ed37dd6649'
|
||||
source = SHEET.read_text(encoding='utf-8')
|
||||
root = parse(source)
|
||||
instance = next(s for s in children(root, 'symbol') if properties(s)['Reference'][2] == 'C737')
|
||||
props = properties(instance)
|
||||
assert props['MPN'][2] == 'CL31A226KAHNNNE' and props['Footprint'][2] == 'PhoneRail:C1206'
|
||||
old_lib_id = child(instance, 'lib_id')[1]
|
||||
template = next(s for s in children(child(root, 'lib_symbols'), 'symbol') if s[1] == old_lib_id)
|
||||
symbol = source[template.start:template.end].replace('CL31A226KAHNNNE', SYMBOL)
|
||||
symbol_root = parse(symbol)
|
||||
graphics = children(symbol_root, 'symbol')[0]
|
||||
plus = '\n(text "+" (at 2 2.8 0) (effects (font (size 1.27 1.27))))\n'
|
||||
symbol = symbol[:graphics.end-1] + plus + symbol[graphics.end-1:]
|
||||
edits = [(child(instance, 'lib_id').start, child(instance, 'lib_id').end, f'(lib_id "PhoneRail:{SYMBOL}")')]
|
||||
for key, value in META.items():
|
||||
p = props[key]
|
||||
raw = source[p.start:p.end]
|
||||
updated = re.sub(r'^(\(property\s+"[^\"]*"\s+)"(?:\\.|[^\"\\])*"',
|
||||
lambda m: m[1] + json.dumps(value), raw)
|
||||
edits.append((p.start, p.end, updated))
|
||||
pos = child(root, 'lib_symbols').end - 1
|
||||
edits.append((pos, pos, '\n' + symbol + '\n'))
|
||||
updated_sheet = update_spans(source, edits)
|
||||
library = LIB.read_text(encoding='utf-8')
|
||||
lib_root = parse(library)
|
||||
updated_lib = library[:lib_root.end-1] + '\n' + symbol.replace('"PhoneRail:' + SYMBOL + '"', '"' + SYMBOL + '"', 1) + '\n' + library[lib_root.end-1:]
|
||||
standard = Path('D:/KiCad/share/kicad/footprints/Capacitor_Tantalum_SMD.pretty/CP_EIA-3216-18_Kemet-A.kicad_mod')
|
||||
footprint = standard.read_text(encoding='utf-8')
|
||||
fp_root = parse(footprint)
|
||||
fp_edits = []
|
||||
for pad in children(fp_root, 'pad'):
|
||||
raw = footprint[pad.start:pad.end]
|
||||
fp_edits.append((pad.start, pad.end, raw.replace('(pad "' + pad[1] + '"', '(pad "' + {'1': '2', '2': '1'}[pad[1]] + '"', 1)))
|
||||
footprint = update_spans(footprint, fp_edits).replace('(footprint "CP_EIA-3216-18_Kemet-A"', '(footprint "' + FP_NAME + '"', 1)
|
||||
table = FP_TABLE.read_text(encoding='utf-8')
|
||||
table_root = parse(table)
|
||||
entry = '\n(lib (name "PhoneRail-Qualified") (type "KiCad") (uri "${KIPRJMOD}/../lib/PhoneRail-Qualified.pretty") (options "") (descr "Datasheet-qualified replacements"))\n'
|
||||
updated_table = table[:table_root.end-1] + entry + table[table_root.end-1:]
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
if not backup.exists():
|
||||
backup.mkdir()
|
||||
Board(2).compile(CLI, backup)
|
||||
for path in (SHEET, BOARD, LIB, FP_TABLE):
|
||||
shutil.copy2(path, backup / path.name)
|
||||
FP_DIR.mkdir(exist_ok=True)
|
||||
(FP_DIR / (FP_NAME + '.kicad_mod')).write_text(footprint, encoding='utf-8')
|
||||
board = pcb.LoadBoard(str(BOARD))
|
||||
old = next(f for f in board.GetFootprints() if f.GetReference() == 'C737')
|
||||
original_pads = {p.GetNumber(): p for p in old.Pads()}
|
||||
f = pcb.FootprintLoad(str(FP_DIR), FP_NAME)
|
||||
board.Add(f)
|
||||
f.SetPosition(old.GetPosition())
|
||||
if old.GetLayer() == pcb.B_Cu:
|
||||
f.Flip(f.GetPosition(), False)
|
||||
scores = []
|
||||
for angle in (0, 90, 180, 270):
|
||||
f.SetOrientationDegrees(angle)
|
||||
scores.append((sum((p.GetPosition() - original_pads[p.GetNumber()].GetPosition()).EuclideanNorm() for p in f.Pads()), angle))
|
||||
f.SetOrientationDegrees(min(scores)[1])
|
||||
f.SetFPIDAsString(META['Footprint'])
|
||||
f.SetReference('C737')
|
||||
f.SetValue(META['Value'])
|
||||
f.SetPath(old.GetPath())
|
||||
f.SetSheetfile(old.GetSheetfile())
|
||||
f.SetSheetname(old.GetSheetname())
|
||||
board.Remove(old)
|
||||
f.SetUuid(pcb.KIID(old.m_Uuid.AsString()))
|
||||
for field in old.GetFields():
|
||||
if field.GetName() not in ('Reference', 'Value', 'Footprint'):
|
||||
f.SetField(field.GetName(), field.GetText())
|
||||
for key, value in META.items():
|
||||
if key not in ('Value', 'Footprint'):
|
||||
f.SetField(key, value)
|
||||
field_style(f, old)
|
||||
for p in f.Pads():
|
||||
p.SetNet(original_pads[p.GetNumber()].GetNet())
|
||||
pcb.SaveBoard(str(BOARD), board)
|
||||
SHEET.write_text(updated_sheet, encoding='utf-8')
|
||||
LIB.write_text(updated_lib, encoding='utf-8')
|
||||
FP_TABLE.write_text(updated_table, encoding='utf-8')
|
||||
(OUT / 'selection.json').write_text(json.dumps({**META, 'symbol': SYMBOL,
|
||||
'positive_pin': '2', 'negative_pin': '1', 'footprint_source': str(standard),
|
||||
'footprint_changes': 'Only root name and pad numbers swapped; standard positive marking remains on positive terminal',
|
||||
'catalog_record': 'out/fan-catalog/20260907T070741439087-detail.json'}, indent=2) + '\n', encoding='utf-8')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--apply', action='store_true')
|
||||
args = parser.parse_args()
|
||||
if args.apply:
|
||||
apply()
|
||||
verify()
|
||||
在新工单中引用
屏蔽一个用户