Skill/scripts/altium_refs.py

170 lines
6.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""Read the internal *reference name* out of Altium symbol (.SchLib) and footprint (.PcbLib)
files, and assemble the four design-column values for a part.
Why this exists: the value that belongs in the sheet's **Library Ref** / **Footprint Ref**
column is NOT the file name or the MPN - it is the component/footprint name as Altium stores
it *inside* the library (what an engineer sees in the file's properties). We read it directly
so no one has to open Altium by hand.
Verified against real Vecmocon files (Ultra Librarian export, e.g. CGA3E3X7R1H474K080AE):
- .SchLib: the component is a top-level OLE *storage* whose name is the Library Ref, e.g.
``CGA3E3X7R1H474K080AE``. (Confirmed also in the FileHeader as ``LibRef0=...``.)
- .PcbLib: each footprint is a top-level storage. An Ultra Librarian export ships the base
pattern plus IPC density variants, e.g. ``CAP_CGA3_TDK`` (nominal), ``CAP_CGA3_TDK-L``
(least/L), ``CAP_CGA3_TDK-M`` (most/M). The **base** pattern (the one the others suffix
with ``-L`` / ``-M`` / ``-N``) is the Footprint Ref -> ``CAP_CGA3_TDK``.
The four values that get written to the sheet:
Library Ref = name inside the .SchLib (read here)
Library Path = the .SchLib file name (just the basename)
Footprint Ref = base pattern inside the .PcbLib (read here)
Footprint Path = the .PcbLib file name (just the basename)
Usage:
# one file -> its reference name
python altium_refs.py CGA3E3X7R1H474K080AE.SchLib
python altium_refs.py CGA3E3X7R1H474K080AE.PcbLib --json
# both files -> ready-to-use design map for fill_templates --design
python altium_refs.py design --symbol X.SchLib --footprint Y.PcbLib > design.json
Programmatic: from altium_refs import extract_ref, build_design
If a ref comes back None or a footprint has several unrelated candidates, ask the engineer to
confirm the name shown in Altium's properties.
"""
import argparse, json, os, re
import olefile
# Entry names that are container metadata, never a component/footprint reference.
META = {
"fileheader", "additionalfileheader", "storage", "library", "root entry",
"componentparamstoc", "fileversioninfo", "sectionkeys", "data", "header",
"uniqueidprimitiveinformation", "pintextdata", "textframe", "models",
"modelsnoembed", "arcs6", "pads6", "vias6", "tracks6", "texts6", "fills6",
"regions6", "componentbodies6", "designatoroverride", "parameters",
"primitiveguids", "widestrings", "layerkindmapping", "padvialibrary",
"embeddedfonts", "textures", "fileversion",
}
def _decode(raw):
for enc in ("utf-16-le", "latin-1", "utf-8"):
try:
return raw.decode(enc, errors="ignore")
except Exception:
continue
return ""
def _storage_names(ole):
"""Distinct top-level storage names that aren't container metadata."""
names = []
for entry in ole.listdir(streams=True, storages=True):
top = entry[0]
if top.lower() in META or top in names:
continue
names.append(top)
return names
def _from_header_records(ole):
"""Backstop: scan header streams for LibReference / PatternName records."""
hits = []
for entry in ole.listdir(streams=True):
if entry[-1].lower() not in ("fileheader", "header", "data"):
continue
try:
txt = _decode(ole.openstream(entry).read())
except Exception:
continue
for key in ("LIBREFERENCE", "PATTERNNAME", "PATTERN"):
for m in re.finditer(rf"{key}=([^\x00|]+)", txt, re.IGNORECASE):
val = m.group(1).strip()
if val and val not in hits:
hits.append(val)
return hits
def _pick_base(candidates):
"""Choose the primary reference. When several names share a base and differ only by a
trailing density suffix (Altium IPC variants: -L / -M / -N / -Least / -Most / -Nominal),
the base (a prefix of the others) is the real one. Otherwise take the first."""
if not candidates:
return None
bases = [c for c in candidates
if any(o != c and o.startswith(c + "-") for o in candidates)]
if bases:
return sorted(bases, key=len)[0]
# no prefix relationship: drop obvious -L/-M/-N variants if a suffix-less peer exists
plain = [c for c in candidates if not re.search(r"-(L|M|N|Least|Most|Nominal)$", c)]
return (plain or candidates)[0]
def extract_ref(path):
"""Return (ref, candidates): ref = best single reference name (base pattern) or None."""
if not olefile.isOleFile(path):
return None, []
ole = olefile.OleFileIO(path)
try:
candidates = _storage_names(ole) or _from_header_records(ole)
finally:
ole.close()
return _pick_base(candidates), candidates
def kind_of(path):
ext = os.path.splitext(path)[1].lower()
return {".schlib": "symbol", ".pcblib": "footprint"}.get(ext, "unknown")
def build_design(symbol_path, footprint_path):
"""Assemble the four design-column values from the two uploaded files."""
lib_ref, lib_cands = extract_ref(symbol_path)
fp_ref, fp_cands = extract_ref(footprint_path)
return {
"Library Ref": lib_ref,
"Library Path": os.path.basename(symbol_path),
"Footprint Ref": fp_ref,
"Footprint Path": os.path.basename(footprint_path),
"_candidates": {"symbol": lib_cands, "footprint": fp_cands},
}
def main():
ap = argparse.ArgumentParser()
sub = ap.add_subparsers(dest="cmd")
d = sub.add_parser("design", help="emit the 4-field design map from both files")
d.add_argument("--symbol", required=True)
d.add_argument("--footprint", required=True)
ap.add_argument("file", nargs="?", help=".SchLib or .PcbLib (single-file mode)")
ap.add_argument("--json", action="store_true")
a = ap.parse_args()
if a.cmd == "design":
design = build_design(a.symbol, a.footprint)
warn = [k for k in ("Library Ref", "Footprint Ref") if not design[k]]
print(json.dumps(design, ensure_ascii=False, indent=2))
if warn:
print(f"# WARNING: could not read {', '.join(warn)} - confirm from Altium", flush=True)
return
if not a.file:
ap.error("give a file, or use: design --symbol X.SchLib --footprint Y.PcbLib")
ref, cands = extract_ref(a.file)
if a.json:
print(json.dumps({"kind": kind_of(a.file), "ref": ref, "candidates": cands}, ensure_ascii=False))
return
if ref is None:
print(f"Could not read a reference from {a.file}. Open it in Altium and use the "
"name shown in properties.")
else:
print(ref)
if len(cands) > 1:
print(f"(variants present: {cands}; using base '{ref}')")
if __name__ == "__main__":
main()