170 lines
6.7 KiB
Python
Executable File
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()
|