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
296 lines
8.3 KiB
Python
296 lines
8.3 KiB
Python
#!/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)
|