#!/usr/bin/env python3 """ gen_resistors_0402.py ===================== Reads the approved parts list spreadsheet and generates KiCad 9 symbols for every 0402 resistor into kicad-lib/symbols/Res_0402.kicad_sym. The existing R_temp template symbol is kept in the file. One new symbol is added per row, named by the internal part number (GLE P/N), with: Value ← Value1 (e.g. "10k", "4.7k", "100") Description ← Description column UMPN ← GLE P/N MFG ← Mfg.1 MPN ← Mfg.1 P/N All geometry and pin definitions are copied verbatim from R_temp. Usage: python3 gen_resistors_0402.py """ import re import sys import pandas as pd from pathlib import Path # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def kicad_str(value: str) -> str: """Escape a string for use inside a KiCad S-expression quoted string.""" return str(value).replace('\\', '\\\\').replace('"', '\\"') def make_symbol(name: str, value: str, description: str, umpn: str, mfg: str, mpn: str, geometry_body: str) -> str: """ Render a complete (symbol ...) block. geometry_body is the text of the two inner sub-symbols (originally named R_temp_0_1 and R_temp_1_1) with the name already substituted. """ return f'''\t(symbol "{kicad_str(name)}" \t\t(pin_numbers \t\t\t(hide yes) \t\t) \t\t(pin_names \t\t\t(offset 0) \t\t) \t\t(exclude_from_sim no) \t\t(in_bom yes) \t\t(on_board yes) \t\t(property "Reference" "R" \t\t\t(at 2.54 0 90) \t\t\t(effects \t\t\t\t(font \t\t\t\t\t(size 1.27 1.27) \t\t\t\t) \t\t\t) \t\t) \t\t(property "Value" "{kicad_str(value)}" \t\t\t(at -2.54 0 90) \t\t\t(effects \t\t\t\t(font \t\t\t\t\t(size 1.27 1.27) \t\t\t\t) \t\t\t) \t\t) \t\t(property "Footprint" "Resistor_SMD:R_0402_1005Metric" \t\t\t(at 1.016 -0.254 90) \t\t\t(effects \t\t\t\t(font \t\t\t\t\t(size 1.27 1.27) \t\t\t\t) \t\t\t\t(hide yes) \t\t\t) \t\t) \t\t(property "Datasheet" "~" \t\t\t(at 0 0 0) \t\t\t(effects \t\t\t\t(font \t\t\t\t\t(size 1.27 1.27) \t\t\t\t) \t\t\t\t(hide yes) \t\t\t) \t\t) \t\t(property "Description" "{kicad_str(description)}" \t\t\t(at 0 0 0) \t\t\t(effects \t\t\t\t(font \t\t\t\t\t(size 1.27 1.27) \t\t\t\t) \t\t\t\t(hide yes) \t\t\t) \t\t) \t\t(property "MPN" "{kicad_str(mpn)}" \t\t\t(at 0 0 0) \t\t\t(effects \t\t\t\t(font \t\t\t\t\t(size 1.27 1.27) \t\t\t\t) \t\t\t\t(hide yes) \t\t\t) \t\t) \t\t(property "MFG" "{kicad_str(mfg)}" \t\t\t(at 0 0 0) \t\t\t(effects \t\t\t\t(font \t\t\t\t\t(size 1.27 1.27) \t\t\t\t) \t\t\t\t(hide yes) \t\t\t) \t\t) \t\t(property "UMPN" "{kicad_str(umpn)}" \t\t\t(at 0 0 0) \t\t\t(effects \t\t\t\t(font \t\t\t\t\t(size 1.27 1.27) \t\t\t\t) \t\t\t\t(hide yes) \t\t\t) \t\t) \t\t(property "ki_keywords" "R res resistor" \t\t\t(at 0 0 0) \t\t\t(effects \t\t\t\t(font \t\t\t\t\t(size 1.27 1.27) \t\t\t\t) \t\t\t\t(hide yes) \t\t\t) \t\t) \t\t(property "ki_fp_filters" "R_*" \t\t\t(at 0 0 0) \t\t\t(effects \t\t\t\t(font \t\t\t\t\t(size 1.27 1.27) \t\t\t\t) \t\t\t\t(hide yes) \t\t\t) \t\t) {geometry_body} \t\t(embedded_fonts no) \t)''' # --------------------------------------------------------------------------- # Extract geometry from R_temp # --------------------------------------------------------------------------- def extract_geometry(sym_text: str) -> str: """ Pull out the two inner sub-symbol blocks (R_temp_0_1 and R_temp_1_1) from the raw file text and return them as a single string with the 'R_temp' name prefix replaced by a placeholder that callers substitute. """ # Match (symbol "R_temp_0_1" ...) and (symbol "R_temp_1_1" ...) # by tracking brace depth after the opening paren of each sub-symbol. results = [] for sub in ('R_temp_0_1', 'R_temp_1_1'): pattern = f'(symbol "{sub}"' start = sym_text.find(pattern) if start == -1: raise ValueError(f"Could not find sub-symbol '{sub}' in template") # Walk forward to find the matching closing paren depth = 0 i = start while i < len(sym_text): if sym_text[i] == '(': depth += 1 elif sym_text[i] == ')': depth -= 1 if depth == 0: results.append(sym_text[start:i + 1]) break i += 1 return '\n'.join(results) # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- def main(xlsx_path: Path, sym_path: Path): # ---- Load spreadsheet ---- df = pd.read_excel(xlsx_path, sheet_name='PCB', dtype=str) df = df.fillna('') # Filter to 0402 resistors mask = ( df['Footprint'].str.contains('0402', na=False) & df['Description'].str.contains('[Rr]es', na=False, regex=True) ) resistors = df[mask].copy() print(f"Found {len(resistors)} 0402 resistors in parts list") # ---- Read existing symbol file ---- existing = sym_path.read_text(encoding='utf-8') # Extract geometry from R_temp once raw_geom = extract_geometry(existing) # ---- Build new symbols ---- new_symbols = [] skipped = [] for _, row in resistors.iterrows(): gle_pn = str(row['GLE P/N']).strip() value = str(row['Value1']).strip() description = str(row['Description']).strip() mfg = str(row['Mfg.1']).strip() mpn = str(row['Mfg.1 P/N']).strip() if not gle_pn: skipped.append(('(no GLE P/N)', value)) continue # Substitute R_temp → this symbol's name in the geometry block. # Also re-indent so sub-symbol opening parens align with the template. geom = raw_geom.replace('R_temp_0_1', f'{gle_pn}_0_1') \ .replace('R_temp_1_1', f'{gle_pn}_1_1') # Each sub-symbol block starts at column 0 after extract; add two tabs. geom = '\n'.join('\t\t' + line if line.startswith('(symbol ') else line for line in geom.splitlines()) sym = make_symbol( name=gle_pn, value=value, description=description, umpn=gle_pn, mfg=mfg, mpn=mpn, geometry_body=geom, ) new_symbols.append(sym) print(f"Generated {len(new_symbols)} symbols ({len(skipped)} skipped)") # ---- Rebuild file from scratch (idempotent) ---- # Extract the library header lines (everything before the first symbol) # and keep only the R_temp template symbol, then append generated symbols. header_end = existing.find('\n\t(symbol "R_temp"') if header_end == -1: raise ValueError("Could not find R_temp template in symbol file") header = existing[:header_end] # Extract R_temp block by brace depth rt_start = existing.index('\n\t(symbol "R_temp"') depth, i = 0, rt_start + 1 # skip leading newline while i < len(existing): if existing[i] == '(': depth += 1 elif existing[i] == ')': depth -= 1 if depth == 0: rt_block = existing[rt_start:i + 1] break i += 1 lib_body = header + rt_block + '\n' + '\n'.join(new_symbols) + '\n)' # Back up original backup = sym_path.with_suffix('.bak') backup.write_text(existing, encoding='utf-8') print(f"Backup written to {backup.name}") sym_path.write_text(lib_body, encoding='utf-8') print(f"Wrote {sym_path}") # Quick sanity check symbol_count = lib_body.count('\n\t(symbol "') print(f"Total symbols in file (including R_temp): {symbol_count}") if __name__ == '__main__': if len(sys.argv) == 3: xlsx = Path(sys.argv[1]) sym = Path(sys.argv[2]) else: # Default paths relative to repo root repo = Path(__file__).parent.parent xlsx = repo.parent / 'parts_list_pcb.xlsx' sym = repo / 'symbols' / 'Res_0402.kicad_sym' if not xlsx.exists(): print(f"Error: spreadsheet not found at {xlsx}", file=sys.stderr) sys.exit(1) if not sym.exists(): print(f"Error: symbol file not found at {sym}", file=sys.stderr) sys.exit(1) main(xlsx, sym)