235 lines
7.1 KiB
Python
235 lines
7.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Parses all .kicad_sym files in symbols/ and updates the Component Index
|
|
table in README.md with the actual contents of the library.
|
|
|
|
Reads the following fields from each symbol:
|
|
Value, MPN, Digikey_PN, Manufacturer, Footprint, ki_description (or Description)
|
|
|
|
Symbol Library column = which .kicad_sym file the symbol lives in.
|
|
Used to locate a symbol quickly in KiCad Symbol Editor.
|
|
|
|
Handles derived symbols (Derive from symbol) by merging parent fields
|
|
with child overrides so all fields resolve correctly.
|
|
|
|
Run locally: python3 .github/scripts/update_component_index.py
|
|
Run in CI: triggered automatically on push to symbols/
|
|
"""
|
|
|
|
import re
|
|
from pathlib import Path
|
|
from datetime import date
|
|
|
|
SYMBOLS_DIR = Path("symbols")
|
|
README_PATH = Path("README.md")
|
|
|
|
CATEGORY_ORDER = [
|
|
"0_ic_logic",
|
|
"0_ic_driver",
|
|
"0_ic_power",
|
|
"0_ic_analog",
|
|
"0_ic_mcu",
|
|
"0_ic_rf",
|
|
"0_ic_interface",
|
|
"0_passive",
|
|
"0_connector",
|
|
"0_discrete",
|
|
]
|
|
|
|
|
|
def extract_all_fields(block: str) -> dict:
|
|
"""Extract all property fields from a symbol block into a dict."""
|
|
fields = {}
|
|
for m in re.finditer(r'\(property\s+"([^"]+)"\s+"([^"]*)"', block):
|
|
fields[m.group(1)] = m.group(2).strip()
|
|
return fields
|
|
|
|
|
|
def shorten_footprint(footprint: str) -> str:
|
|
"""Trim long footprint names for table readability."""
|
|
if footprint and ":" in footprint:
|
|
lib_part, fp_name = footprint.split(":", 1)
|
|
if len(fp_name) > 30:
|
|
fp_name = fp_name[:27] + "..."
|
|
return f"{lib_part}:{fp_name}"
|
|
return footprint
|
|
|
|
|
|
def parse_symbols_from_file(filepath: Path) -> list:
|
|
"""
|
|
Parse all symbols from a .kicad_sym file.
|
|
|
|
Handles both normal symbols and derived symbols (Derive from symbol).
|
|
Derived symbols only store fields that differ from their parent — the
|
|
parser merges parent fields first then overlays child overrides so all
|
|
fields resolve correctly.
|
|
"""
|
|
content = filepath.read_text(encoding="utf-8")
|
|
|
|
# --- Pass 1: collect ALL raw symbol blocks indexed by name ---
|
|
symbol_pattern = re.compile(r'\(symbol\s+"([^"]+)"', re.MULTILINE)
|
|
all_matches = list(symbol_pattern.finditer(content))
|
|
|
|
raw_blocks = {} # name -> raw text block
|
|
extends_map = {} # child name -> parent name
|
|
|
|
for i, match in enumerate(all_matches):
|
|
name = match.group(1)
|
|
# Skip KiCad internal sub-unit blocks e.g. SN74HC193DR_0_1
|
|
if re.search(r'_\d+_\d+$', name):
|
|
continue
|
|
start = match.start()
|
|
end = all_matches[i + 1].start() if i + 1 < len(all_matches) else len(content)
|
|
block = content[start:end]
|
|
raw_blocks[name] = block
|
|
|
|
extends_match = re.search(r'\(extends\s+"([^"]+)"\)', block)
|
|
if extends_match:
|
|
extends_map[name] = extends_match.group(1)
|
|
|
|
# --- Pass 2: resolve fields, merging parent into child ---
|
|
def resolve_fields(name, visited=None):
|
|
if visited is None:
|
|
visited = set()
|
|
if name in visited:
|
|
return {}
|
|
visited.add(name)
|
|
block = raw_blocks.get(name, "")
|
|
own_fields = extract_all_fields(block)
|
|
parent_name = extends_map.get(name)
|
|
if parent_name:
|
|
parent_fields = resolve_fields(parent_name, visited)
|
|
return {**parent_fields, **own_fields}
|
|
return own_fields
|
|
|
|
# --- Pass 3: build symbol list ---
|
|
symbols = []
|
|
|
|
for name in raw_blocks:
|
|
is_derived = name in extends_map
|
|
fields = resolve_fields(name)
|
|
mpn = fields.get("MPN") or fields.get("Value") or name
|
|
|
|
# Skip base/template symbols — no MPN field, just drawing sources
|
|
if not is_derived and not fields.get("MPN"):
|
|
continue
|
|
|
|
description = (fields.get("ki_description")
|
|
or fields.get("Description")
|
|
or fields.get("description")
|
|
or "—")
|
|
footprint = shorten_footprint(fields.get("Footprint", "")) or "-"
|
|
digikey_pn = fields.get("Digikey_PN") or fields.get("Digikey PN") or "-"
|
|
manufacturer = fields.get("Manufacturer") or "-"
|
|
|
|
symbols.append({
|
|
"name": name,
|
|
"mpn": mpn,
|
|
"description": description,
|
|
"footprint": footprint,
|
|
"digikey_pn": digikey_pn,
|
|
"manufacturer": manufacturer,
|
|
})
|
|
|
|
return symbols
|
|
|
|
|
|
def build_component_table(all_symbols: dict) -> str:
|
|
"""Build the full markdown component index table."""
|
|
lines = []
|
|
lines.append("| MPN | Description | Manufacturer | Symbol Library | Footprint | Digikey PN |")
|
|
lines.append("|---|---|---|---|---|---|")
|
|
|
|
total = 0
|
|
for category in CATEGORY_ORDER:
|
|
symbols = all_symbols.get(category, [])
|
|
if not symbols:
|
|
continue
|
|
for s in sorted(symbols, key=lambda x: x["mpn"]):
|
|
lines.append(
|
|
f"| {s['mpn']} "
|
|
f"| {s['description']} "
|
|
f"| {s['manufacturer']} "
|
|
f"| {category} "
|
|
f"| {s['footprint']} "
|
|
f"| {s['digikey_pn']} |"
|
|
)
|
|
total += 1
|
|
|
|
lines.append("")
|
|
lines.append(f"*{total} components — auto-generated {date.today().isoformat()}*")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def update_readme(new_table: str) -> bool:
|
|
"""
|
|
Replace the Component Index section in README.md with the new table.
|
|
Returns True if the file was changed.
|
|
"""
|
|
readme = README_PATH.read_text(encoding="utf-8")
|
|
|
|
pattern = re.compile(
|
|
r'(## Component Index\n+)(.*?)(\n## |\Z)',
|
|
re.DOTALL
|
|
)
|
|
|
|
match = pattern.search(readme)
|
|
if not match:
|
|
print("ERROR: Could not find '## Component Index' section in README.md")
|
|
return False
|
|
|
|
original_block = match.group(2)
|
|
new_block = new_table + "\n\n"
|
|
|
|
if original_block == new_block:
|
|
return False
|
|
|
|
updated_readme = (
|
|
readme[:match.start(2)]
|
|
+ new_block
|
|
+ readme[match.start(3):]
|
|
)
|
|
|
|
README_PATH.write_text(updated_readme, encoding="utf-8")
|
|
return True
|
|
|
|
|
|
def main():
|
|
if not SYMBOLS_DIR.exists():
|
|
print("ERROR: symbols/ directory not found. Run from repo root.")
|
|
return
|
|
|
|
if not README_PATH.exists():
|
|
print("ERROR: README.md not found. Run from repo root.")
|
|
return
|
|
|
|
all_symbols = {}
|
|
|
|
for sym_file in sorted(SYMBOLS_DIR.glob("*.kicad_sym")):
|
|
lib_name = sym_file.stem
|
|
symbols = parse_symbols_from_file(sym_file)
|
|
if symbols:
|
|
all_symbols[lib_name] = symbols
|
|
print(f" {lib_name}: {len(symbols)} symbol(s)")
|
|
else:
|
|
print(f" {lib_name}: empty")
|
|
|
|
if not any(all_symbols.values()):
|
|
print("No symbols found — README not updated.")
|
|
return
|
|
|
|
total = sum(len(v) for v in all_symbols.values())
|
|
print(f"\nTotal: {total} symbols across {len(all_symbols)} libraries")
|
|
|
|
table = build_component_table(all_symbols)
|
|
changed = update_readme(table)
|
|
|
|
if changed:
|
|
print("\nREADME.md component index updated.")
|
|
else:
|
|
print("\nREADME.md already up to date.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|