schlib_write: write full template+SOP param set (--typeid), fill ComponentDescription from template Description; docs updated

main
admin 2026-07-14 13:19:37 +00:00
parent 2ee9ebc6b5
commit 6751926ac2
No known key found for this signature in database
4 changed files with 89 additions and 19 deletions

View File

@ -340,10 +340,14 @@ Leave any genuinely-unknown field blank — the SOP hides blank parameters, so a
empty until filled. The full method for the second-source search is in empty until filled. The full method for the second-source search is in
`references/schlib_parameters.md`. `references/schlib_parameters.md`.
Collect the values into a `params.json` and write them into the symbol: Write the **full parameter set** — the typeid template's engineering columns **plus** the SOP
params above (see `references/schlib_parameters.md`). Collect your filled values into a
`params.json` and pass `--typeid` so the writer guarantees every template column is present
(blank where the datasheet is silent):
```bash ```bash
python scripts/schlib_write.py --schlib <in>.SchLib --params params.json --out <stage>/<tag>/<sym>.SchLib python scripts/schlib_write.py --schlib <in>.SchLib --params params.json \
--out <stage>/<tag>/<sym>.SchLib --typeid <TYPEID>
``` ```
Deliver the resulting `.SchLib`; the engineer opens it in Altium once to confirm it loads, then Deliver the resulting `.SchLib`; the engineer opens it in Altium once to confirm it loads, then

Binary file not shown.

View File

@ -93,24 +93,35 @@ symbol's Library Ref (from `altium_refs.py`); omit it to apply to every componen
} }
``` ```
## The full parameter set (template + SOP)
Every `.SchLib` should carry the **complete** parameter set for its part: the **typeid
template's engineering columns** (all columns of that typeid's `template.xlsx` sheet except the
internal bookkeeping ones — the tag `MPN_make_type`, `Skill Version`, `Template Version`, and the
four `Library/Footprint Ref/Path` columns) **plus** the mandatory SOP params above. So a CER
(ceramic MLCC) symbol gets `Capacitance(uF)`, `Tolerance`, `Voltage(V)`,
`Dielectric(temp. Coefficient)`, `Operating Temp(°C)`, `Max operating temp(°C)`, `Package`,
`Description`, `Manufacturer` from the template, alongside `Value`, `Manufacturer Part`,
`Process`, `Vecmocon Part Code`, `ROHS`, `Datasheet`, and the second-source fields. Fill each
from the datasheet; leave blank what the datasheet doesn't state.
## Writing them into the symbol ## Writing them into the symbol
Write the parameters straight into the `.SchLib`, producing a new file: Write the parameters straight into the `.SchLib`, producing a new file. Pass `--typeid` so the
writer guarantees the whole template column set is present (blank where you didn't supply a
value) — this is what keeps every symbol's parameter set complete and consistent:
```bash ```bash
python scripts/schlib_write.py --schlib <in>.SchLib --params params.json --out <out>.SchLib python scripts/schlib_write.py --schlib <in>.SchLib --params params.json --out <out>.SchLib --typeid <TYPEID>
``` ```
`params.json` may carry a `"remove"` list (defaults to `["Manufacturer_Name", `params.json` carries your filled values (and may include a `"remove"` list — defaults to
"Manufacturer_Part_Number"]`); those Ultra-Librarian defaults are stripped and the SOP params `["Manufacturer_Name", "Manufacturer_Part_Number"]`, the Ultra-Librarian duplicates that get
added. The script targets the component named in `"component"` (its Library Ref / storage name), stripped). The script targets the component named in `"component"` (its Library Ref / storage
or every component if omitted, and self-checks the output re-opens as a valid OLE. Deliver the name), or every component if omitted, and self-checks the output re-opens as a valid OLE. It
resulting `.SchLib`, and have the engineer open it in Altium once to confirm it loads, then Save handles any parameter-set size — small sets stay in Altium's mini-stream, larger ones are written
to Server with a revision note per the SOP. as a regular stream automatically. Deliver the resulting `.SchLib`; have the engineer open it in
Altium once to confirm it loads, then Save to Server with a revision note per the SOP.
Scope note: the direct writer keeps a component's `Data` under Altium's 4096-byte mini-stream
threshold in the common case; a very large parameter set (or an extremely long datasheet URL)
can push it past that, at which point fall back to the `altium_params.py` DXP-script path.
Fallback (apply from inside Altium): Fallback (apply from inside Altium):

View File

@ -29,7 +29,7 @@ IMPORTANT: this writes Altium's own binary format from outside Altium. It is val
re-open as a well-formed OLE with every other stream byte-identical, but ALWAYS open the result re-open as a well-formed OLE with every other stream byte-identical, but ALWAYS open the result
in Altium once to confirm it loads before relying on it. in Altium once to confirm it loads before relying on it.
""" """
import argparse, json, struct, sys, hashlib import argparse, json, os, struct, sys, hashlib
import olefile import olefile
FREESECT=0xFFFFFFFF; ENDOFCHAIN=0xFFFFFFFE; FATSECT=0xFFFFFFFD FREESECT=0xFFFFFFFF; ENDOFCHAIN=0xFFFFFFFE; FATSECT=0xFFFFFFFD
@ -42,6 +42,26 @@ def le32(b,o): return struct.unpack('<I',b[o:o+4])[0]
def pad(b,n): return b+b'\x00'*((-len(b))%n) def pad(b,n): return b+b'\x00'*((-len(b))%n)
# ----------------------------------------------------------------- typeid template columns
# Template columns that are internal library bookkeeping, NOT symbol parameters.
NON_PARAM_COLS = {"MPN_make_type", "Skill Version", "Template Version",
"Library Ref", "Library Path", "Footprint Ref", "Footprint Path"}
def template_param_names(template_path, typeid):
"""The symbol-parameter columns for a typeid = every column on that typeid's template sheet
EXCEPT the internal bookkeeping ones (the tag, the two version columns, and the four design
Ref/Path columns). These are the engineering parameters that belong on the symbol."""
import openpyxl
wb = openpyxl.load_workbook(template_path, read_only=True)
if typeid not in wb.sheetnames:
raise SystemExit(f"no template sheet for typeid '{typeid}'")
ws = wb[typeid]
return [ws.cell(1, c).value for c in range(1, ws.max_column + 1)
if ws.cell(1, c).value and ws.cell(1, c).value not in NON_PARAM_COLS]
# ----------------------------------------------------------------- read the container # ----------------------------------------------------------------- read the container
def read_container(path): def read_container(path):
@ -118,16 +138,34 @@ def _rec_name(block):
return t.split('|Name=')[1].split('|')[0] if '|Name=' in t else None return t.split('|Name=')[1].split('|')[0] if '|Name=' in t else None
def _patch_field(block, field, value):
"""Replace |field=...| inside a length-prefixed text record, re-framing its 4-byte length.
Used to set the component's ComponentDescription in the RECORD=1 header."""
import re
text = block[4:-1].decode('latin-1')
if f"|{field}=" in text:
text = re.sub(rf"\|{re.escape(field)}=[^|]*", f"|{field}={value}", text, count=1)
elif text.startswith("|RECORD="):
text = text + f"|{field}={value}"
payload = text.encode('latin-1') + b'\x00'
return struct.pack('<I', len(payload)) + payload
def edit_data(data, params, remove): def edit_data(data, params, remove):
"""Return a new Data stream: drop `remove` params, drop any SOP-name params (re-added """Return a new Data stream: drop `remove` params, drop any SOP-name params (re-added
fresh), keep everything else, then append the SOP params. Pins/graphics/tail untouched.""" fresh), keep everything else, then append the SOP params. Also mirror the `Description`
parameter into the component's ComponentDescription field (the Altium 'Description' shown in
the component properties). Pins/graphics/tail untouched."""
leading, tail = _leading_text_records(data) leading, tail = _leading_text_records(data)
sop_names=set(params) sop_names=set(params)
desc = params.get("Description")
kept=[] kept=[]
for blk in leading: for blk in leading:
nm=_rec_name(blk) nm=_rec_name(blk)
if nm is not None and (nm in remove or nm in sop_names): if nm is not None and (nm in remove or nm in sop_names):
continue # drop UL duplicates + stale SOP copies continue # drop UL duplicates + stale SOP copies
if desc is not None and blk[4:-1].startswith(b"|RECORD=1|"):
blk = _patch_field(blk, "ComponentDescription", desc) # component Description field
kept.append(blk) kept.append(blk)
added=[_param_record(20+i, nm, val) for i,(nm,val) in enumerate(params.items())] added=[_param_record(20+i, nm, val) for i,(nm,val) in enumerate(params.items())]
return b''.join(kept)+b''.join(added)+tail return b''.join(kept)+b''.join(added)+tail
@ -211,10 +249,20 @@ def rebuild(entries, content):
# ----------------------------------------------------------------- driver # ----------------------------------------------------------------- driver
def write_params(schlib, params_json, out): def write_params(schlib, params_json, out, typeid=None, template=None):
component=params_json.get("component") or None component=params_json.get("component") or None
fields=params_json.get("parameters", {}) or {} fields=dict(params_json.get("parameters", {}) or {})
remove=params_json.get("remove", DEFAULT_REMOVE) remove=params_json.get("remove", DEFAULT_REMOVE)
# If a typeid+template are given, guarantee the FULL template parameter set is written:
# every engineering column for that typeid becomes a symbol parameter (value from the
# params if provided, else blank). This is what makes every .SchLib carry the complete,
# consistent parameter set the template defines — not just whatever was hand-listed.
typeid = typeid or params_json.get("typeid")
template = template or params_json.get("template")
if typeid and template:
for name in template_param_names(template, typeid):
fields.setdefault(name, "")
entries, paths, content = read_container(schlib) entries, paths, content = read_container(schlib)
# target Data stream sid(s): a stream named 'Data' whose parent storage == component (or all) # target Data stream sid(s): a stream named 'Data' whose parent storage == component (or all)
@ -253,8 +301,15 @@ def main():
ap.add_argument("--schlib", required=True) ap.add_argument("--schlib", required=True)
ap.add_argument("--params", required=True) ap.add_argument("--params", required=True)
ap.add_argument("--out", required=True) ap.add_argument("--out", required=True)
ap.add_argument("--typeid", help="component typeid; with --template, writes that typeid's "
"full template parameter set (blank where not provided)")
ap.add_argument("--template", help="path to template.xlsx (defaults to the skill's)")
a=ap.parse_args() a=ap.parse_args()
write_params(a.schlib, json.load(open(a.params, encoding="utf-8")), a.out) template = a.template
if a.typeid and not template:
template = os.path.join(os.path.dirname(__file__), "..", "assets", "template", "template.xlsx")
write_params(a.schlib, json.load(open(a.params, encoding="utf-8")), a.out,
typeid=a.typeid, template=template)
if __name__ == "__main__": if __name__ == "__main__":