adding libraries
This commit is contained in:
0
footprints/footprints.pretty/CONN_BM05B-PASS-1_JST.kicad_mod → footprints/um-footprints.pretty/CONN_BM05B-PASS-1_JST.kicad_mod
Executable file → Normal file
0
footprints/footprints.pretty/CONN_BM05B-PASS-1_JST.kicad_mod → footprints/um-footprints.pretty/CONN_BM05B-PASS-1_JST.kicad_mod
Executable file → Normal file
0
footprints/footprints.pretty/LQFP100_14X14_NXP.kicad_mod → footprints/um-footprints.pretty/LQFP100_14X14_NXP.kicad_mod
Executable file → Normal file
0
footprints/footprints.pretty/LQFP100_14X14_NXP.kicad_mod → footprints/um-footprints.pretty/LQFP100_14X14_NXP.kicad_mod
Executable file → Normal file
1839
footprints/um-footprints.pretty/MXA20A_TEX.kicad_mod
Normal file
1839
footprints/um-footprints.pretty/MXA20A_TEX.kicad_mod
Normal file
File diff suppressed because it is too large
Load Diff
0
footprints/footprints.pretty/QFN16_MG_MCH.kicad_mod → footprints/um-footprints.pretty/QFN16_MG_MCH.kicad_mod
Executable file → Normal file
0
footprints/footprints.pretty/QFN16_MG_MCH.kicad_mod → footprints/um-footprints.pretty/QFN16_MG_MCH.kicad_mod
Executable file → Normal file
0
footprints/footprints.pretty/SO16_TOS.kicad_mod → footprints/um-footprints.pretty/SO16_TOS.kicad_mod
Executable file → Normal file
0
footprints/footprints.pretty/SO16_TOS.kicad_mod → footprints/um-footprints.pretty/SO16_TOS.kicad_mod
Executable file → Normal file
94
libs.txt
Normal file
94
libs.txt
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
Batteries
|
||||||
|
BGA
|
||||||
|
Capacitor
|
||||||
|
Cap_0402
|
||||||
|
Cap_0603
|
||||||
|
Cap_0805
|
||||||
|
Cap_1206
|
||||||
|
Cap_Alum_SMD
|
||||||
|
Cap_Alum_TH
|
||||||
|
Cap_Array
|
||||||
|
Cap_Film
|
||||||
|
Cap_Misc
|
||||||
|
Cap_Precision
|
||||||
|
Cap_RF
|
||||||
|
Cap_Tantalum
|
||||||
|
Connector
|
||||||
|
Connectors
|
||||||
|
Conn_0-50mm
|
||||||
|
Conn_1-00mm
|
||||||
|
Conn_1-25mm
|
||||||
|
Conn_1-27mm
|
||||||
|
Conn_2-00mm
|
||||||
|
Conn_2-00mm_DF11
|
||||||
|
Conn_2-00mm_DF3
|
||||||
|
Conn_2-54mm
|
||||||
|
Conn_DF11
|
||||||
|
Conn_Misc
|
||||||
|
Conn_RF
|
||||||
|
Conn_Test
|
||||||
|
default
|
||||||
|
DFN
|
||||||
|
Diode
|
||||||
|
DIP
|
||||||
|
Disc_BJT
|
||||||
|
Disc_Diode
|
||||||
|
Disc_FET
|
||||||
|
Disc_Zener_TVS
|
||||||
|
Doc_sch
|
||||||
|
Footprints
|
||||||
|
Fuse
|
||||||
|
Fuses
|
||||||
|
IC_ADC
|
||||||
|
IC_Analog
|
||||||
|
IC_Audio
|
||||||
|
IC_DAC
|
||||||
|
IC_Driver
|
||||||
|
IC_Interface
|
||||||
|
IC_Logic
|
||||||
|
IC_MCU
|
||||||
|
IC_Memory
|
||||||
|
IC_Misc
|
||||||
|
IC_OpAmp
|
||||||
|
IC_Power
|
||||||
|
IC_Power_Linear
|
||||||
|
IC_Power_SMPS
|
||||||
|
IC_Power_SW_Cap
|
||||||
|
IC_RF
|
||||||
|
IC_Sensor
|
||||||
|
Inductor
|
||||||
|
Inductor_0402
|
||||||
|
Inductor_SMD
|
||||||
|
Inductor_TH
|
||||||
|
Info
|
||||||
|
LCD
|
||||||
|
LED
|
||||||
|
Library_Toolkit
|
||||||
|
Mech
|
||||||
|
Obsolete
|
||||||
|
Opto
|
||||||
|
PCB-Archive
|
||||||
|
pcb-doc
|
||||||
|
PCB
|
||||||
|
QFN
|
||||||
|
Relay
|
||||||
|
Resistor
|
||||||
|
Res_0402
|
||||||
|
Res_0603
|
||||||
|
Res_0805
|
||||||
|
Res_1206
|
||||||
|
Res_Array
|
||||||
|
Res_Misc
|
||||||
|
RF
|
||||||
|
RF_Module
|
||||||
|
Sensor
|
||||||
|
SMT
|
||||||
|
SOT
|
||||||
|
SQFP
|
||||||
|
Switch
|
||||||
|
Symbol
|
||||||
|
Test
|
||||||
|
Tools
|
||||||
|
Transistor
|
||||||
|
VFD
|
||||||
|
XTAL
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
BOM CSV Exporter
|
|
||||||
================
|
|
||||||
Reads extract_symbols.json and writes bom.csv with columns:
|
|
||||||
Reference, MPN, MFG
|
|
||||||
|
|
||||||
Power symbols (#PWR, #FLG) are excluded.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python3 export_bom.py [project_dir]
|
|
||||||
"""
|
|
||||||
|
|
||||||
import csv
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
def main(project_dir: Path):
|
|
||||||
json_path = project_dir / 'extract_symbols.json'
|
|
||||||
if not json_path.exists():
|
|
||||||
print(f"Error: {json_path} not found. Run extract_symbols.py first.", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
data = json.loads(json_path.read_text(encoding='utf-8'))
|
|
||||||
symbols = data['symbols']
|
|
||||||
|
|
||||||
# Filter out power/flag symbols, DNP parts, and parts excluded from BOM
|
|
||||||
parts = [
|
|
||||||
s for s in symbols
|
|
||||||
if not (s.get('reference') or '').startswith(('#', '~'))
|
|
||||||
and not (s.get('lib_id') or '').startswith('power:')
|
|
||||||
and s.get('in_bom') is not False
|
|
||||||
and not s.get('dnp')
|
|
||||||
]
|
|
||||||
|
|
||||||
# Collapse multi-unit / duplicate records to one row per reference.
|
|
||||||
# If multiple records exist for the same ref, pick the one with the
|
|
||||||
# most complete MPN/MFG data (longest non-placeholder string).
|
|
||||||
def data_score(s):
|
|
||||||
props = s.get('properties', {})
|
|
||||||
mpn = props.get('MPN', '')
|
|
||||||
mfg = props.get('MFG') or props.get('MANUFACTURER', '')
|
|
||||||
placeholder = mpn.strip().lower() in ('', 'x', 'tbd', 'n/a', 'na')
|
|
||||||
return (0 if placeholder else len(mpn) + len(mfg))
|
|
||||||
|
|
||||||
from collections import defaultdict
|
|
||||||
by_ref: dict[str, list] = defaultdict(list)
|
|
||||||
for s in parts:
|
|
||||||
by_ref[s.get('reference', '')].append(s)
|
|
||||||
|
|
||||||
best: list[dict] = []
|
|
||||||
for ref, recs in by_ref.items():
|
|
||||||
best.append(max(recs, key=data_score))
|
|
||||||
|
|
||||||
# Sort by reference
|
|
||||||
def ref_sort_key(r):
|
|
||||||
ref = r.get('reference') or ''
|
|
||||||
letters = ''.join(c for c in ref if c.isalpha())
|
|
||||||
digits = ''.join(c for c in ref if c.isdigit())
|
|
||||||
return (letters, int(digits) if digits else 0)
|
|
||||||
|
|
||||||
best.sort(key=ref_sort_key)
|
|
||||||
|
|
||||||
out_path = project_dir / 'bom.csv'
|
|
||||||
with out_path.open('w', newline='', encoding='utf-8') as f:
|
|
||||||
writer = csv.writer(f)
|
|
||||||
writer.writerow(['Reference', 'MPN', 'MFG'])
|
|
||||||
for s in best:
|
|
||||||
props = s.get('properties', {})
|
|
||||||
writer.writerow([
|
|
||||||
s.get('reference', ''),
|
|
||||||
props.get('MPN', ''),
|
|
||||||
props.get('MFG') or props.get('MANUFACTURER', ''),
|
|
||||||
])
|
|
||||||
|
|
||||||
excluded_bom = sum(1 for s in symbols if s.get('in_bom') is False
|
|
||||||
and not (s.get('reference') or '').startswith(('#', '~')))
|
|
||||||
excluded_dnp = sum(1 for s in symbols if s.get('dnp')
|
|
||||||
and not (s.get('reference') or '').startswith(('#', '~'))
|
|
||||||
and s.get('in_bom') is not False)
|
|
||||||
print(f"Excluded: {excluded_bom} 'exclude from BOM', {excluded_dnp} DNP")
|
|
||||||
print(f"Wrote {len(best)} unique references to {out_path} (collapsed from {len(parts)} records)")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
project_dir = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else Path(__file__).parent.resolve()
|
|
||||||
main(project_dir)
|
|
||||||
@@ -1,517 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
KiCad 9 Symbol Metadata Extractor
|
|
||||||
==================================
|
|
||||||
Walks every .kicad_sch file in the project directory and extracts
|
|
||||||
metadata for every placed symbol (component instance), correctly
|
|
||||||
expanding hierarchical sheet instances so that each unique reference
|
|
||||||
in the final design becomes its own record.
|
|
||||||
|
|
||||||
KiCad stores multi-instance sheets by embedding an `(instances ...)`
|
|
||||||
block in each symbol. That block contains one `(path ...)` entry per
|
|
||||||
sheet instantiation, each with the authoritative reference for that
|
|
||||||
copy. This script reads those paths so a sheet used N times produces
|
|
||||||
N distinct records per symbol.
|
|
||||||
|
|
||||||
Output: extract_symbols.json (same directory as this script)
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python3 extract_symbols.py [project_dir]
|
|
||||||
|
|
||||||
If project_dir is omitted, the directory containing this script is used.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# S-expression parser
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def _tokenize(text: str) -> list:
|
|
||||||
"""
|
|
||||||
Convert raw KiCad S-expression text into a flat list of tokens.
|
|
||||||
Token forms:
|
|
||||||
('OPEN',) – opening paren
|
|
||||||
('CLOSE',) – closing paren
|
|
||||||
('ATOM', value) – unquoted word / number / bool
|
|
||||||
('STR', value) – double-quoted string (escapes resolved)
|
|
||||||
"""
|
|
||||||
tokens = []
|
|
||||||
i, n = 0, len(text)
|
|
||||||
while i < n:
|
|
||||||
c = text[i]
|
|
||||||
if c in ' \t\r\n':
|
|
||||||
i += 1
|
|
||||||
elif c == '(':
|
|
||||||
tokens.append(('OPEN',))
|
|
||||||
i += 1
|
|
||||||
elif c == ')':
|
|
||||||
tokens.append(('CLOSE',))
|
|
||||||
i += 1
|
|
||||||
elif c == '"':
|
|
||||||
j = i + 1
|
|
||||||
buf = []
|
|
||||||
while j < n:
|
|
||||||
if text[j] == '\\' and j + 1 < n:
|
|
||||||
buf.append(text[j + 1])
|
|
||||||
j += 2
|
|
||||||
elif text[j] == '"':
|
|
||||||
j += 1
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
buf.append(text[j])
|
|
||||||
j += 1
|
|
||||||
tokens.append(('STR', ''.join(buf)))
|
|
||||||
i = j
|
|
||||||
else:
|
|
||||||
j = i
|
|
||||||
while j < n and text[j] not in ' \t\r\n()':
|
|
||||||
j += 1
|
|
||||||
tokens.append(('ATOM', text[i:j]))
|
|
||||||
i = j
|
|
||||||
return tokens
|
|
||||||
|
|
||||||
|
|
||||||
def _parse(tokens: list, pos: int) -> tuple:
|
|
||||||
"""
|
|
||||||
Recursively parse one S-expression value starting at *pos*.
|
|
||||||
Returns (parsed_value, next_pos).
|
|
||||||
A list/node becomes a Python list; atoms and strings become strings.
|
|
||||||
"""
|
|
||||||
tok = tokens[pos]
|
|
||||||
kind = tok[0]
|
|
||||||
if kind == 'OPEN':
|
|
||||||
pos += 1
|
|
||||||
items = []
|
|
||||||
while tokens[pos][0] != 'CLOSE':
|
|
||||||
item, pos = _parse(tokens, pos)
|
|
||||||
items.append(item)
|
|
||||||
return items, pos + 1 # consume CLOSE
|
|
||||||
elif kind in ('ATOM', 'STR'):
|
|
||||||
return tok[1], pos + 1
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unexpected token at pos {pos}: {tok}")
|
|
||||||
|
|
||||||
|
|
||||||
def parse_sexp(text: str):
|
|
||||||
"""Parse a complete KiCad S-expression file. Returns the root list."""
|
|
||||||
tokens = _tokenize(text)
|
|
||||||
root, _ = _parse(tokens, 0)
|
|
||||||
return root
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Helpers to navigate parsed S-expressions
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def tag(node) -> str:
|
|
||||||
if isinstance(node, list) and node and isinstance(node[0], str):
|
|
||||||
return node[0]
|
|
||||||
return ''
|
|
||||||
|
|
||||||
|
|
||||||
def children(node: list) -> list:
|
|
||||||
return node[1:] if isinstance(node, list) else []
|
|
||||||
|
|
||||||
|
|
||||||
def first_child_with_tag(node: list, name: str):
|
|
||||||
for child in children(node):
|
|
||||||
if isinstance(child, list) and tag(child) == name:
|
|
||||||
return child
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def all_children_with_tag(node: list, name: str) -> list:
|
|
||||||
return [c for c in children(node) if isinstance(c, list) and tag(c) == name]
|
|
||||||
|
|
||||||
|
|
||||||
def scalar(node, index: int = 1, default=None):
|
|
||||||
if isinstance(node, list) and len(node) > index:
|
|
||||||
return node[index]
|
|
||||||
return default
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Instance path extraction
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def extract_instances(sym_node: list) -> list[dict]:
|
|
||||||
"""
|
|
||||||
Parse the (instances ...) block of a symbol and return one dict per
|
|
||||||
hierarchical path. Each dict has:
|
|
||||||
path – the full UUID path string
|
|
||||||
reference – the reference designator for that instance
|
|
||||||
unit – the unit number for that instance
|
|
||||||
project – the project name
|
|
||||||
|
|
||||||
If there is no instances block (unusual), returns an empty list.
|
|
||||||
"""
|
|
||||||
instances_node = first_child_with_tag(sym_node, 'instances')
|
|
||||||
if instances_node is None:
|
|
||||||
return []
|
|
||||||
|
|
||||||
results = []
|
|
||||||
for project_node in all_children_with_tag(instances_node, 'project'):
|
|
||||||
project_name = scalar(project_node, 1, '')
|
|
||||||
for path_node in all_children_with_tag(project_node, 'path'):
|
|
||||||
path_str = scalar(path_node, 1, '')
|
|
||||||
ref_node = first_child_with_tag(path_node, 'reference')
|
|
||||||
unit_node = first_child_with_tag(path_node, 'unit')
|
|
||||||
results.append({
|
|
||||||
'path': path_str,
|
|
||||||
'reference': scalar(ref_node, 1) if ref_node else None,
|
|
||||||
'unit': scalar(unit_node, 1) if unit_node else None,
|
|
||||||
'project': project_name,
|
|
||||||
})
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Symbol extraction
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def extract_symbol_records(sym_node: list, sheet_file: str) -> list[dict]:
|
|
||||||
"""
|
|
||||||
Extract metadata from a placed-symbol node and return one record per
|
|
||||||
hierarchical instance (i.e. one record per path in the instances block).
|
|
||||||
|
|
||||||
For a sheet used only once, this produces a single record.
|
|
||||||
For a sheet instantiated N times, this produces N records — each with
|
|
||||||
its own unique reference designator from the instances block.
|
|
||||||
"""
|
|
||||||
# --- Shared fields (same for all instances of this symbol placement) ---
|
|
||||||
shared = {
|
|
||||||
'sheet_file': sheet_file,
|
|
||||||
'lib_id': None,
|
|
||||||
'at': None,
|
|
||||||
'exclude_from_sim': None,
|
|
||||||
'in_bom': None,
|
|
||||||
'on_board': None,
|
|
||||||
'dnp': None,
|
|
||||||
'uuid': None,
|
|
||||||
'properties': {},
|
|
||||||
}
|
|
||||||
|
|
||||||
for child in children(sym_node):
|
|
||||||
if not isinstance(child, list):
|
|
||||||
continue
|
|
||||||
t = tag(child)
|
|
||||||
if t == 'lib_id':
|
|
||||||
shared['lib_id'] = scalar(child, 1)
|
|
||||||
elif t == 'at':
|
|
||||||
shared['at'] = {
|
|
||||||
'x': scalar(child, 1),
|
|
||||||
'y': scalar(child, 2),
|
|
||||||
'angle': scalar(child, 3, 0),
|
|
||||||
}
|
|
||||||
elif t == 'exclude_from_sim':
|
|
||||||
shared['exclude_from_sim'] = scalar(child, 1) == 'yes'
|
|
||||||
elif t == 'in_bom':
|
|
||||||
shared['in_bom'] = scalar(child, 1) == 'yes'
|
|
||||||
elif t == 'on_board':
|
|
||||||
shared['on_board'] = scalar(child, 1) == 'yes'
|
|
||||||
elif t == 'dnp':
|
|
||||||
shared['dnp'] = scalar(child, 1) == 'yes'
|
|
||||||
elif t == 'uuid':
|
|
||||||
shared['uuid'] = scalar(child, 1)
|
|
||||||
elif t == 'property':
|
|
||||||
prop_name = scalar(child, 1)
|
|
||||||
prop_val = scalar(child, 2)
|
|
||||||
if prop_name is not None:
|
|
||||||
shared['properties'][prop_name] = prop_val
|
|
||||||
|
|
||||||
# Promote standard properties for convenient access
|
|
||||||
props = shared['properties']
|
|
||||||
shared['value'] = props.get('Value')
|
|
||||||
shared['footprint'] = props.get('Footprint')
|
|
||||||
shared['datasheet'] = props.get('Datasheet')
|
|
||||||
shared['description'] = props.get('Description')
|
|
||||||
|
|
||||||
# --- Per-instance fields (one record per path in instances block) ---
|
|
||||||
instances = extract_instances(sym_node)
|
|
||||||
|
|
||||||
if not instances:
|
|
||||||
# Fallback: no instances block — use top-level Reference property
|
|
||||||
record = dict(shared)
|
|
||||||
record['reference'] = props.get('Reference')
|
|
||||||
record['instance_path'] = None
|
|
||||||
record['instance_unit'] = shared.get('unit')
|
|
||||||
record['instance_project']= None
|
|
||||||
return [record]
|
|
||||||
|
|
||||||
records = []
|
|
||||||
for inst in instances:
|
|
||||||
record = dict(shared)
|
|
||||||
record['properties'] = dict(shared['properties']) # copy so each is independent
|
|
||||||
record['reference'] = inst['reference']
|
|
||||||
record['instance_path'] = inst['path']
|
|
||||||
record['instance_unit'] = inst['unit']
|
|
||||||
record['instance_project'] = inst['project']
|
|
||||||
records.append(record)
|
|
||||||
|
|
||||||
return records
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Hierarchy walker
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def find_reachable_sheets(root_sch: Path) -> list[Path]:
|
|
||||||
"""
|
|
||||||
Walk the sheet hierarchy starting from *root_sch* and return an ordered
|
|
||||||
list of every .kicad_sch file that is actually reachable (i.e. referenced
|
|
||||||
directly or transitively as a sub-sheet). Handles repeated sub-sheet
|
|
||||||
references (same file used N times) by visiting the file only once.
|
|
||||||
"""
|
|
||||||
reachable: list[Path] = []
|
|
||||||
visited_names: set[str] = set()
|
|
||||||
queue: list[Path] = [root_sch]
|
|
||||||
|
|
||||||
while queue:
|
|
||||||
sch = queue.pop(0)
|
|
||||||
if sch.name in visited_names:
|
|
||||||
continue
|
|
||||||
visited_names.add(sch.name)
|
|
||||||
reachable.append(sch)
|
|
||||||
|
|
||||||
try:
|
|
||||||
text = sch.read_text(encoding='utf-8')
|
|
||||||
except OSError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
root_node = parse_sexp(text)
|
|
||||||
for child in children(root_node):
|
|
||||||
if tag(child) != 'sheet':
|
|
||||||
continue
|
|
||||||
for prop in all_children_with_tag(child, 'property'):
|
|
||||||
if scalar(prop, 1) == 'Sheetfile':
|
|
||||||
child_filename = scalar(prop, 2)
|
|
||||||
if child_filename:
|
|
||||||
child_path = sch.parent / child_filename
|
|
||||||
if child_path.exists() and child_path.name not in visited_names:
|
|
||||||
queue.append(child_path)
|
|
||||||
|
|
||||||
return reachable
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Per-file parsing
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def extract_from_schematic(sch_path: Path) -> list[dict]:
|
|
||||||
"""
|
|
||||||
Parse one .kicad_sch file and return a list of symbol records.
|
|
||||||
lib_symbols definitions are skipped; only placed instances are returned.
|
|
||||||
"""
|
|
||||||
text = sch_path.read_text(encoding='utf-8')
|
|
||||||
root = parse_sexp(text)
|
|
||||||
|
|
||||||
results = []
|
|
||||||
for child in children(root):
|
|
||||||
if not isinstance(child, list):
|
|
||||||
continue
|
|
||||||
t = tag(child)
|
|
||||||
if t == 'lib_symbols':
|
|
||||||
continue # skip library definitions
|
|
||||||
if t == 'symbol' and first_child_with_tag(child, 'lib_id') is not None:
|
|
||||||
records = extract_symbol_records(child, sch_path.name)
|
|
||||||
results.extend(records)
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Main
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def get_root_uuid(project_dir: Path) -> str | None:
|
|
||||||
"""
|
|
||||||
Find the UUID of the root schematic by reading the .kicad_pro file
|
|
||||||
(which names the root sheet) or by scanning for the top-level sheet.
|
|
||||||
Returns the UUID string, or None if it cannot be determined.
|
|
||||||
"""
|
|
||||||
# The .kicad_pro file tells us the root schematic filename
|
|
||||||
pro_files = list(project_dir.glob('*.kicad_pro'))
|
|
||||||
root_sch: Path | None = None
|
|
||||||
|
|
||||||
if pro_files:
|
|
||||||
import json as _json
|
|
||||||
try:
|
|
||||||
pro = _json.loads(pro_files[0].read_text(encoding='utf-8'))
|
|
||||||
root_name = pro.get('sheets', [{}])[0] if pro.get('sheets') else None
|
|
||||||
# Fall back: just find a .kicad_sch with the same stem as the .pro
|
|
||||||
root_sch = project_dir / (pro_files[0].stem + '.kicad_sch')
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if root_sch is None or not root_sch.exists():
|
|
||||||
# Guess: the .kicad_sch whose stem matches the .kicad_pro
|
|
||||||
if pro_files:
|
|
||||||
candidate = project_dir / (pro_files[0].stem + '.kicad_sch')
|
|
||||||
if candidate.exists():
|
|
||||||
root_sch = candidate
|
|
||||||
|
|
||||||
if root_sch is None or not root_sch.exists():
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Extract the first (uuid ...) at the root level of the file
|
|
||||||
import re
|
|
||||||
text = root_sch.read_text(encoding='utf-8')
|
|
||||||
m = re.search(r'\(uuid\s+"([^"]+)"', text)
|
|
||||||
return m.group(1) if m else None
|
|
||||||
|
|
||||||
|
|
||||||
def main(project_dir: Path):
|
|
||||||
# Determine root schematic and walk the real hierarchy
|
|
||||||
root_uuid = get_root_uuid(project_dir)
|
|
||||||
|
|
||||||
pro_files = list(project_dir.glob('*.kicad_pro'))
|
|
||||||
root_sch = project_dir / (pro_files[0].stem + '.kicad_sch') if pro_files else None
|
|
||||||
|
|
||||||
if root_sch and root_sch.exists():
|
|
||||||
sch_files = find_reachable_sheets(root_sch)
|
|
||||||
print(f"Root sheet: {root_sch.name}")
|
|
||||||
print(f"Found {len(sch_files)} reachable schematic file(s) in hierarchy:")
|
|
||||||
else:
|
|
||||||
# Fallback: glob everything
|
|
||||||
sch_files = sorted(
|
|
||||||
p for p in project_dir.rglob('*.kicad_sch')
|
|
||||||
if not p.name.startswith('_autosave')
|
|
||||||
and not p.suffix.endswith('.bak')
|
|
||||||
)
|
|
||||||
print(f"Warning: could not find root schematic; scanning all {len(sch_files)} files.\n")
|
|
||||||
|
|
||||||
if not sch_files:
|
|
||||||
print(f"No .kicad_sch files found in {project_dir}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
for f in sch_files:
|
|
||||||
print(f" {f.relative_to(project_dir)}")
|
|
||||||
|
|
||||||
all_records: list[dict] = []
|
|
||||||
|
|
||||||
for sch_path in sch_files:
|
|
||||||
print(f"\nParsing {sch_path.name} ...", end=' ', flush=True)
|
|
||||||
records = extract_from_schematic(sch_path)
|
|
||||||
print(f"{len(records)} instance record(s)")
|
|
||||||
all_records.extend(records)
|
|
||||||
|
|
||||||
# All records come from reachable sheets, so no orphan filtering needed.
|
|
||||||
# Optionally still filter by root UUID to catch stale instance paths.
|
|
||||||
if root_uuid:
|
|
||||||
active_prefix = f'/{root_uuid}/'
|
|
||||||
active = [r for r in all_records
|
|
||||||
if (r.get('instance_path') or '').startswith(active_prefix)]
|
|
||||||
stale = len(all_records) - len(active)
|
|
||||||
print(f"\nTotal records : {len(all_records)}")
|
|
||||||
if stale:
|
|
||||||
print(f"Stale paths dropped: {stale}")
|
|
||||||
else:
|
|
||||||
active = all_records
|
|
||||||
print(f"\nTotal records: {len(all_records)}")
|
|
||||||
|
|
||||||
# ---- Stage 1: dedup by (instance_path, uuid) ----
|
|
||||||
# Collapses records that were seen from multiple sheet scans into one.
|
|
||||||
seen: set = set()
|
|
||||||
stage1: list[dict] = []
|
|
||||||
for r in active:
|
|
||||||
key = (r.get('instance_path'), r.get('uuid'))
|
|
||||||
if key not in seen:
|
|
||||||
seen.add(key)
|
|
||||||
stage1.append(r)
|
|
||||||
|
|
||||||
# ---- Stage 2: dedup by uuid across different sheet files ----
|
|
||||||
# If the SAME uuid appears in two *different* .kicad_sch files, that is a
|
|
||||||
# UUID collision in the design (copy-paste without UUID regeneration).
|
|
||||||
# The same uuid appearing in the same sheet file with different instance
|
|
||||||
# paths is *correct* — it is how multi-instance sheets work, so those are
|
|
||||||
# left alone.
|
|
||||||
uuid_sheets: dict = {} # uuid -> set of sheet_files seen
|
|
||||||
uuid_collisions: dict = {} # uuid -> list of colliding records
|
|
||||||
unique: list[dict] = []
|
|
||||||
for r in stage1:
|
|
||||||
u = r.get('uuid')
|
|
||||||
sf = r.get('sheet_file', '')
|
|
||||||
sheets_so_far = uuid_sheets.setdefault(u, set())
|
|
||||||
if not sheets_so_far or sf in sheets_so_far:
|
|
||||||
# First time seeing this uuid, OR it's from the same sheet file
|
|
||||||
# (legitimate multi-instance expansion) — keep it.
|
|
||||||
sheets_so_far.add(sf)
|
|
||||||
unique.append(r)
|
|
||||||
else:
|
|
||||||
# Same uuid, but from a DIFFERENT sheet file → UUID collision.
|
|
||||||
uuid_collisions.setdefault(u, []).append(r)
|
|
||||||
# Don't append to unique — drop the duplicate.
|
|
||||||
|
|
||||||
if uuid_collisions:
|
|
||||||
print(f"\nNote: {len(uuid_collisions)} UUID collision(s) detected "
|
|
||||||
f"(same symbol UUID in multiple sheet files — likely copy-paste artifacts).")
|
|
||||||
print(" Only the first occurrence is kept in the output.")
|
|
||||||
for u, recs in list(uuid_collisions.items())[:10]:
|
|
||||||
refs = [r.get('reference') for r in recs]
|
|
||||||
files = [r.get('sheet_file') for r in recs]
|
|
||||||
print(f" uuid={u[:8]}... refs={refs} sheets={files}")
|
|
||||||
|
|
||||||
print(f"\nUnique instances after dedup: {len(unique)}")
|
|
||||||
|
|
||||||
# Separate power symbols from real parts
|
|
||||||
real = [r for r in unique if not (r.get('lib_id') or '').startswith('power:')]
|
|
||||||
power = [r for r in unique if (r.get('lib_id') or '').startswith('power:')]
|
|
||||||
print(f" Non-power parts : {len(real)}")
|
|
||||||
print(f" Power symbols : {len(power)}")
|
|
||||||
|
|
||||||
# Check for true reference duplicates (same ref, different uuid = multi-unit)
|
|
||||||
from collections import defaultdict, Counter
|
|
||||||
by_ref: dict[str, list] = defaultdict(list)
|
|
||||||
for r in unique:
|
|
||||||
by_ref[r.get('reference', '')].append(r)
|
|
||||||
|
|
||||||
multi_unit = {ref: recs for ref, recs in by_ref.items()
|
|
||||||
if len(recs) > 1 and len({r['uuid'] for r in recs}) > 1}
|
|
||||||
if multi_unit:
|
|
||||||
refs = [r for r in multi_unit if not r.startswith('#')]
|
|
||||||
if refs:
|
|
||||||
print(f"\nMulti-unit components ({len(refs)} references, expected for split-unit symbols):")
|
|
||||||
for ref in sorted(refs):
|
|
||||||
units = [r['instance_unit'] for r in multi_unit[ref]]
|
|
||||||
print(f" {ref}: units {units}")
|
|
||||||
|
|
||||||
output = {
|
|
||||||
"project_dir": str(project_dir),
|
|
||||||
"root_uuid": root_uuid,
|
|
||||||
"schematic_files": [str(f.relative_to(project_dir)) for f in sch_files],
|
|
||||||
"total_instances": len(unique),
|
|
||||||
"non_power_count": len(real),
|
|
||||||
"symbols": unique,
|
|
||||||
}
|
|
||||||
|
|
||||||
out_path = project_dir / 'extract_symbols.json'
|
|
||||||
out_path.write_text(json.dumps(output, indent=2, ensure_ascii=False), encoding='utf-8')
|
|
||||||
print(f"\nOutput written to: {out_path}")
|
|
||||||
|
|
||||||
# Print a summary table
|
|
||||||
print("\n--- Summary (non-power parts, sorted by reference) ---")
|
|
||||||
for r in sorted(real, key=lambda x: x.get('reference') or ''):
|
|
||||||
ref = r.get('reference', '')
|
|
||||||
value = r.get('value', '')
|
|
||||||
lib = r.get('lib_id', '')
|
|
||||||
mpn = r['properties'].get('MPN', '')
|
|
||||||
sheet = r.get('sheet_file', '')
|
|
||||||
unit = r.get('instance_unit', '')
|
|
||||||
print(f" {ref:<12} u{unit:<2} {value:<30} {lib:<40} MPN={mpn:<25} [{sheet}]")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
if len(sys.argv) > 1:
|
|
||||||
project_dir = Path(sys.argv[1]).resolve()
|
|
||||||
else:
|
|
||||||
project_dir = Path(__file__).parent.resolve()
|
|
||||||
|
|
||||||
if not project_dir.is_dir():
|
|
||||||
print(f"Error: {project_dir} is not a directory", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
main(project_dir)
|
|
||||||
@@ -1,338 +0,0 @@
|
|||||||
#!/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 unique value+tolerance combination, named descriptively, e.g.:
|
|
||||||
R0402_4.7K_1%
|
|
||||||
R0402_100_5%
|
|
||||||
R0402_10.0K_0.1%
|
|
||||||
|
|
||||||
Symbol fields:
|
|
||||||
Value ← Value1 (e.g. "10k", "4.7k", "100")
|
|
||||||
Description ← Description column
|
|
||||||
UMPN ← GLE P/N (internal part number of first approved vendor)
|
|
||||||
MFG ← Mfg.1
|
|
||||||
MPN ← Mfg.1 P/N
|
|
||||||
|
|
||||||
Where multiple approved vendors share the same value+tolerance, only the
|
|
||||||
first row is used (the duplicate rows are reported and skipped).
|
|
||||||
|
|
||||||
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(value: str, description: str) -> str:
|
|
||||||
"""
|
|
||||||
Build a descriptive KiCad symbol name, e.g. 'R0402_4.7K_1%'.
|
|
||||||
|
|
||||||
value – the Value1 column (e.g. '4.7k', '100', '10.0k')
|
|
||||||
description – the Description column (e.g. 'Resistor, 0402, 1%, 4.7k')
|
|
||||||
|
|
||||||
The value suffix (k, m, g) is uppercased. Tolerance is extracted
|
|
||||||
with a regex that matches patterns like '1%', '0.1%', '5%'.
|
|
||||||
"""
|
|
||||||
# Uppercase trailing unit letter(s): 4.7k → 4.7K, 10.0k → 10.0K
|
|
||||||
value_norm = re.sub(r'([a-zA-Z]+)$', lambda m: m.group(1).upper(), value.strip())
|
|
||||||
# Extract tolerance from description
|
|
||||||
tol_match = re.search(r'(\d+(?:\.\d+)?%)', description)
|
|
||||||
tolerance = tol_match.group(1) if tol_match else 'X'
|
|
||||||
return f'R0402_{value_norm}_{tolerance}'
|
|
||||||
|
|
||||||
|
|
||||||
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 = []
|
|
||||||
seen_names: dict[str, str] = {} # sym_name → GLE P/N of first occurrence
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# Build descriptive symbol name, e.g. "R0402_4.7K_1%"
|
|
||||||
sym_name = make_symbol_name(value, description)
|
|
||||||
|
|
||||||
# Skip duplicate value+tolerance combinations (alternate approved vendors)
|
|
||||||
if sym_name in seen_names:
|
|
||||||
skipped.append((sym_name, f'dup of GLE {seen_names[sym_name]} (this: {gle_pn})'))
|
|
||||||
continue
|
|
||||||
seen_names[sym_name] = gle_pn
|
|
||||||
|
|
||||||
# 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'{sym_name}_0_1') \
|
|
||||||
.replace('R_temp_1_1', f'{sym_name}_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=sym_name,
|
|
||||||
value=value,
|
|
||||||
description=description,
|
|
||||||
umpn=gle_pn,
|
|
||||||
mfg=mfg,
|
|
||||||
mpn=mpn,
|
|
||||||
geometry_body=geom,
|
|
||||||
)
|
|
||||||
new_symbols.append(sym)
|
|
||||||
|
|
||||||
dups = [(n, r) for n, r in skipped if n != '(no GLE P/N)']
|
|
||||||
no_pn = [(n, r) for n, r in skipped if n == '(no GLE P/N)']
|
|
||||||
print(f"Generated {len(new_symbols)} symbols "
|
|
||||||
f"({len(dups)} duplicate value/tol skipped, {len(no_pn)} missing GLE P/N)")
|
|
||||||
if dups:
|
|
||||||
print(" Skipped duplicates:")
|
|
||||||
for name, reason in dups:
|
|
||||||
print(f" {name}: {reason}")
|
|
||||||
|
|
||||||
# ---- 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)
|
|
||||||
5
symbols/BGA.kicad_sym
Normal file
5
symbols/BGA.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/Batteries.kicad_sym
Normal file
5
symbols/Batteries.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/Cap_0402.kicad_sym
Normal file
5
symbols/Cap_0402.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/Cap_0603.kicad_sym
Normal file
5
symbols/Cap_0603.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/Cap_0805.kicad_sym
Normal file
5
symbols/Cap_0805.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/Cap_1206.kicad_sym
Normal file
5
symbols/Cap_1206.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/Cap_Alum_SMD.kicad_sym
Normal file
5
symbols/Cap_Alum_SMD.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/Cap_Alum_TH.kicad_sym
Normal file
5
symbols/Cap_Alum_TH.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/Cap_Array.kicad_sym
Normal file
5
symbols/Cap_Array.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/Cap_Film.kicad_sym
Normal file
5
symbols/Cap_Film.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/Cap_Misc.kicad_sym
Normal file
5
symbols/Cap_Misc.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/Cap_Precision.kicad_sym
Normal file
5
symbols/Cap_Precision.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/Cap_RF.kicad_sym
Normal file
5
symbols/Cap_RF.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/Cap_Tantalum.kicad_sym
Normal file
5
symbols/Cap_Tantalum.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/Capacitor.kicad_sym
Normal file
5
symbols/Capacitor.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/Conn_0-50mm.kicad_sym
Normal file
5
symbols/Conn_0-50mm.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/Conn_1-00mm.kicad_sym
Normal file
5
symbols/Conn_1-00mm.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/Conn_1-25mm.kicad_sym
Normal file
5
symbols/Conn_1-25mm.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/Conn_1-27mm.kicad_sym
Normal file
5
symbols/Conn_1-27mm.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/Conn_2-00mm.kicad_sym
Normal file
5
symbols/Conn_2-00mm.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/Conn_2-00mm_DF11.kicad_sym
Normal file
5
symbols/Conn_2-00mm_DF11.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/Conn_2-00mm_DF3.kicad_sym
Normal file
5
symbols/Conn_2-00mm_DF3.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/Conn_2-54mm.kicad_sym
Normal file
5
symbols/Conn_2-54mm.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/Conn_DF11.kicad_sym
Normal file
5
symbols/Conn_DF11.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/Conn_Misc.kicad_sym
Normal file
5
symbols/Conn_Misc.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/Conn_RF.kicad_sym
Normal file
5
symbols/Conn_RF.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/Conn_Test.kicad_sym
Normal file
5
symbols/Conn_Test.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/Connector.kicad_sym
Normal file
5
symbols/Connector.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/Connectors.kicad_sym
Normal file
5
symbols/Connectors.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/DFN.kicad_sym
Normal file
5
symbols/DFN.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/DIP.kicad_sym
Normal file
5
symbols/DIP.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/Diode.kicad_sym
Normal file
5
symbols/Diode.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/Disc_BJT.kicad_sym
Normal file
5
symbols/Disc_BJT.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/Disc_Diode.kicad_sym
Normal file
5
symbols/Disc_Diode.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
5
symbols/Disc_FET.kicad_sym
Normal file
5
symbols/Disc_FET.kicad_sym
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(kicad_symbol_lib
|
||||||
|
(version 20241209)
|
||||||
|
(generator "kicad_symbol_editor")
|
||||||
|
(generator_version "9.0")
|
||||||
|
)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user