"""Apply the catalog-backed FAN-R2 part selection to the existing R1 schematic.""" import json from pathlib import Path import shutil from schematic_maintenance import ROOT, parse, children, child, properties from revise_four_wire_fans import prop, quote, uid def main(): selection = json.loads((ROOT/'fan-selection.json').read_text(encoding='utf-8')) path = ROOT/'PhoneRail-Board1/P6-FAN-EDL.kicad_sch' source = path.read_text(encoding='utf-8') if 'SANYO FAN SELECTION R2' in source: print('FAN-R2 already applied; skipped') return assert 'FOUR-WIRE FAN PROTOTYPE R1' in source root = parse(source) edits = [] for symbol in children(root, 'symbol'): p = properties(symbol) ref = p['Reference'][2] values = {} if ref in {'J26', 'J27'}: values = {'Value': '1725672', 'MPN': '1725672', 'Manufacturer': 'Phoenix Contact', 'LCSC': 'C3816889', 'Footprint': selection['connector']['footprint'], 'Datasheet': 'https://item.szlcsc.com/datasheet/1725672/4384563.html', 'Fan_MPN': selection['mpn'], 'Fan_LCSC': selection['lcsc'], 'Fan_Device_UUID': selection['easyeda_device_uuid'], 'Fan_Datasheet': selection['datasheet_url'], 'Harness': '1 Black GND; 2 Red +12V_SW; 3 Yellow TACH; 4 Brown PWM', 'Description': 'Four-way screw terminal for off-board SANYO fan; fan is in fan-assembly-bom.csv'} if ref in {'R745', 'R749'}: values = {'Value': '15k', 'MPN': '0402WGF1502TCE', 'LCSC': 'C25756'} # C25756 cross-checked through the live catalog on 2026-09-07. x, y = map(float, child(symbol, 'at')[1:3]) extra = [] for key, value in values.items(): if key in p: node = p[key] raw = source[node.start:node.end] old = quote(node[2]) prefix = '(property '+quote(key)+' ' assert raw.startswith(prefix+old) edits.append((node.start, node.end, prefix+quote(value)+raw[len(prefix+old):])) else: extra.append(prop(key, value, x, y, True)) if extra: edits.append((symbol.end-1, symbol.end-1, '\n'.join(extra))) if ref in {'C738', 'D2'}: for key, dy in [('Reference', -2.54), ('Value', 0)]: node = child(p[key], 'at') edits.append((node.start, node.end, f'(at {x+22.86:g} {y+dy:g} 270)')) replacements = { 'FOUR-WIRE FAN PROTOTYPE R1': 'SANYO FAN SELECTION R2 / 2x 9G0612P4S001 / C2920592', '2x NF-A6x25 PWM / J26 intake / J27 exhaust. Vendor limits and prototype tests OPEN.': 'J26 intake / J27 exhaust. 12V, 60x60x25mm, 25kHz PWM. Startup/thermal verification OPEN.', 'Keyed PC fan header: 1 GND / 2 +12V / 3 TACH / 4 PWM.': 'Screw terminals: 1 BLACK GND / 2 RED +12V / 3 YELLOW TACH / 4 BROWN PWM.', 'Confirm header drawing, key orientation and cable fit before layout.': 'Bare AWG26 leads, 300-350mm. No PC-fan plug. 0% PWM still runs at about 3300RPM.', '25kHz target; invert MCU duty: D_gpio = 1 - D_fan. Confirm vendor limits.': '25kHz nominal; invert MCU duty: D_gpio = 1 - D_fan. Family PWM limits: see FAN_CIRCUIT.md.', 'RAW high: Vgate ~ Vfan/3. RAW low: Vgate ~ Vtach_low/2.': 'R745/R749 = 15k: Vgate high ~ Vfan/3.5; low <=0.24V nominal for RAW <=0.6V.', } for note in children(root, 'text'): if note[1] in replacements: raw = source[note.start:note.end] edits.append((note.start, note.end, raw.replace(quote(note[1]), quote(replacements[note[1]]), 1))) rev = child(child(root, 'title_block'), 'rev') edits.append((rev.start, rev.end, '(rev "FAN-R2")')) for start, end, value in sorted(edits, reverse=True): source = source[:start]+value+source[end:] source = source.replace('FanHeader', 'FanTerminal') parse(source) backup = ROOT/'out/before-sanyo-selection' backup.mkdir(exist_ok=False) shutil.copy2(path, backup/path.name) path.write_text(source, encoding='utf-8', newline='\n') lib = ROOT/'lib/PhoneRail-Fan.kicad_sym' lib.write_text(lib.read_text(encoding='utf-8').replace('FanHeader', 'FanTerminal'), encoding='utf-8', newline='\n') name = selection['connector']['footprint'].split(':')[1] fp = Path('D:/KiCad/share/kicad/footprints/TerminalBlock_Phoenix.pretty')/(name+'.kicad_mod') shutil.copy2(fp, ROOT/'lib/PhoneRail-Fan.pretty'/fp.name) print('Applied SANYO fan metadata, Phoenix terminals and 15k TACH gate resistors') if __name__ == '__main__': main()