Initial library + 694 generated 0402 resistor symbols
Symbols (2 libraries): - symbols/ultra-mini.kicad_sym - symbols/M8Mini.kicad_sym - symbols/Res_0402.kicad_sym (R_temp template + 694 generated symbols) Footprints (33 used in ultra project): - footprints/custom.pretty/ (30 mods) - footprints/M8Mini.pretty/ (3 mods) 3D models (31 STEP/STP files) Scripts: - scripts/extract_symbols.py KiCad 9 symbol metadata extractor - scripts/export_bom.py BOM CSV exporter - scripts/gen_resistors_0402.py 0402 resistor symbol generator from parts list
This commit is contained in:
295
scripts/gen_resistors_0402.py
Normal file
295
scripts/gen_resistors_0402.py
Normal file
@@ -0,0 +1,295 @@
|
||||
#!/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 <parts_list.xlsx> <Res_0402.kicad_sym>
|
||||
"""
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user