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:
Brent Perteet
2026-02-20 15:25:43 +00:00
commit 3131535942
71 changed files with 420531 additions and 0 deletions

View 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)