diff --git a/SKILL.md b/SKILL.md index bcd0922..5159e39 100644 --- a/SKILL.md +++ b/SKILL.md @@ -290,6 +290,12 @@ python scripts/fill_templates.py part.json \ --template assets/template/template.xlsx --dest // --design design.json ``` +Once you have the symbol and its Library Ref, you can also produce the **mandatory symbol +parameters** the SOP requires on the `.SchLib` (Manufacturer, Manufacturer Part, Value, +Tolerance, Operating Temperature, RoHS, Datasheet, Process, Vecmocon Part Code, …) — see +*Mandatory symbol parameters* below. This is optional per run but is how the datasheet values +land in the Altium symbol's properties. + ### 7. Assemble the part folder The staging folder `/` should now hold the four files: the per-part `.xlsx`, the @@ -306,6 +312,47 @@ This places the folder at `components///` — creating the Class fol doesn't exist yet, or pushing into it if it does — and commits and pushes. Confirm to the user where it landed. +## Mandatory symbol parameters (.SchLib) + +The SOP (§5) requires every schematic symbol to carry a fixed parameter set in its Altium +properties — `Manufacturer`, `Manufacturer Part`, `Value`, `Tolerance`, `Operating +Temperature`, `ROHS`, `Datasheet`, `Process`, `Vecmocon Part Code`, and the two second-source +fields — with the **Comment** set to the MPN. The skill can stamp these onto the symbol from +the datasheet. + +The skill writes these **directly into the `.SchLib` in pure Python** with +`scripts/schlib_write.py` (it rebuilds the OLE around the enlarged `Data` stream, preserving +every other byte, and strips the Ultra-Librarian `Manufacturer_Name` / `Manufacturer_Part_Number` +defaults that duplicate the SOP fields). Three kinds of value: + +- **Read from the datasheet** (don't just echo given text — open the PDF and fill the real, + verified values): `Value` = the **value only** in shorthand (e.g. `1u`, `12p`, `10k` — no + package), plus `Manufacturer`, `Manufacturer Part`, `Operating Temperature`, `Tolerance`, + `ROHS`, `Datasheet`, and `Process` by inference from the package. +- **Leave blank for now** — `Manufacturer 2` / `Manufacturer Part 2` (the second source). Don't + populate these by default; they stay hidden in Altium until filled later. (An optional + cross-reference search to find a second source is documented in + `references/schlib_parameters.md` but is currently off — only do it if the engineer asks.) +- **Ask the engineer** — only `Vecmocon Part Code` (internal, not derivable). + +Leave any genuinely-unknown field blank — the SOP hides blank parameters, so a gap simply stays +empty until filled. The full method for the second-source search is in +`references/schlib_parameters.md`. + +Collect the values into a `params.json` and write them into the symbol: + +```bash +python scripts/schlib_write.py --schlib .SchLib --params params.json --out //.SchLib +``` + +Deliver the resulting `.SchLib`; the engineer opens it in Altium once to confirm it loads, then +**Saves to Server** with a revision note. The full parameter set, each value's source, the +`params.json` shape (incl. the `remove` list), and the mini-stream size caveat are in +`references/schlib_parameters.md` — read it before building the parameter set. If a file doesn't +round-trip, fall back to `scripts/altium_params.py` (emits an Altium DXP script to apply the same +parameters from inside Altium). Always have the engineer confirm the file opens in Altium — the +skill writes Altium's own binary format, so Altium is the final validator. + ## Per-typeid versioning Versioning is **per typeid**, not global. Each typeid carries its own `template_version` and @@ -462,6 +509,9 @@ plain flat push, but it does not merge the changelog or blank the token, so pref engineering string for each part's Description column). Defines a format for **every** type: the four SOP-defined ones (RES/CAP/Zener/TVS) are strict, the rest are the house extension on the same basis. Read it before filling any Description. +- `references/schlib_parameters.md` — the SOP **mandatory symbol parameters** (§5) for the + `.SchLib`: the parameter set, where each value comes from, and how the generated Altium script + stamps them onto the symbol. - `assets/template/versions.json` — per-typeid `template_version` + `skill_version`. - `assets/CHANGELOG.xlsx` — global version/parameter changelog (created on first add; merged into the skill repo's copy in Gitea by `push-skill`). @@ -472,6 +522,11 @@ plain flat push, but it does not merge the changelog or blank the token, so pref - `scripts/append_parameter.py` — append parameter(s) to a typeid, bump its versions, write the changelog. - `scripts/altium_refs.py` — read Library/Footprint Ref from `.SchLib`/`.PcbLib`. +- `scripts/schlib_write.py` — write the SOP mandatory parameters **directly into a `.SchLib`** + (pure-Python OLE rebuild; removes the Ultra-Librarian `Manufacturer_Name` / + `Manufacturer_Part_Number` defaults). Primary path; see `references/schlib_parameters.md`. +- `scripts/altium_params.py` — fallback: generate an Altium DelphiScript that stamps the same + parameters onto a `.SchLib` from inside Altium (DXP → Run Script). - `scripts/gitea_components.py` — `check-mpn`, `find-part` (locate an existing part to update), `checkout`, `list-type`, `place-part`, `commit-push`, `push-part` (library repo), and `push-skill` (skill repo: token-blanked push + append-only changelog merge). diff --git a/assets/CHANGELOG.xlsx b/assets/CHANGELOG.xlsx index cdd51ab..888a6a8 100644 Binary files a/assets/CHANGELOG.xlsx and b/assets/CHANGELOG.xlsx differ diff --git a/references/schlib_parameters.md b/references/schlib_parameters.md new file mode 100644 index 0000000..9b53436 --- /dev/null +++ b/references/schlib_parameters.md @@ -0,0 +1,122 @@ +# Mandatory symbol parameters (.SchLib) + +Vecmocon's Altium SOP (§5) requires every schematic-library symbol to carry a fixed set of +parameters in its component properties (the panel shown in Altium: *Properties → Parameters*). +This file defines that set, where each value comes from, and how the skill stamps them onto the +`.SchLib` symbol. + +How the parameters get in: the skill writes them **directly into the `.SchLib` in pure Python** +via `scripts/schlib_write.py` — it rebuilds the OLE compound file around the enlarged component +`Data` stream while preserving every other byte (all other streams, the directory tree, the +Altium CLSIDs). It also **removes the Ultra-Librarian default params `Manufacturer_Name` and +`Manufacturer_Part_Number`**, which just duplicate the SOP `Manufacturer` / `Manufacturer Part`. +The output is a ready `.SchLib`. Because this writes Altium's own format from outside Altium, the +script self-checks that the result re-opens as a valid OLE with the params present — but **always +open the result in Altium once to confirm it loads** before relying on it. (An older path, +`scripts/altium_params.py`, instead emits an Altium DXP script to stamp the same parameters from +inside Altium; keep it as a fallback if a particular file doesn't round-trip.) + +## The parameter set + +Use these exact Altium parameter names (they must match the symbol, per the SOP screenshot). +The **Comment** field is set to the MPN (SOP §4), and the **Description** field is the strict +string from `references/description_format.md`. + +| Parameter | Source | Notes | +|-----------|--------|-------| +| `Value` | datasheet | the component **value only** (no package), in shorthand — e.g. `1u`, `12p`, `100n`, `10k` | +| `Manufacturer Part` | datasheet | the MPN; also the **Comment** field | +| `Manufacturer` | datasheet | manufacturer name as printed, e.g. `YAGEO` | +| `Manufacturer Part 2` | **leave blank (for now)** | second-source MPN — left empty by default; see optional note below | +| `Manufacturer 2` | **leave blank (for now)** | second-source manufacturer — left empty by default | +| `Process` | derived | assembly process from package: SMD → `Reflow`, through-hole → `Wave`/`Manual`; confirm | +| `Vecmocon Part Code` | engineer | internal code (e.g. `VECESC2421`) — **not on the datasheet**; ask | +| `Operating Temperature` | datasheet | full range, e.g. `-55 °C to +125 °C` | +| `Tolerance` | datasheet | e.g. `1%` (or `±1%`) | +| `Datasheet` | datasheet | source URL or document reference (SOP marks this optional) | +| `ROHS` | datasheet | RoHS compliance, `Yes`/`No` | + +**Read the datasheet-sourced values from the actual datasheet — don't echo whatever text was +handed to you.** Open the PDF, find each real value (`Value`, `Manufacturer Part`, +`Manufacturer`, `Operating Temperature`, `Tolerance`, `Datasheet`, `ROHS`, and `Process` by +inference from the package), and fill them verified. An honest blank beats a guess — the SOP +hides blank parameters, so a gap just stays empty until someone fills it. Only **one** field is +purely internal and must come from the engineer: `Vecmocon Part Code` — ask for it. + +The second-source pair (`Manufacturer 2` / `Manufacturer Part 2`) is **left blank for now** — +don't populate it by default. It simply stays hidden in Altium until someone fills it later. + +## Optional: second-source cross-reference (currently off) + +**House preference right now is to leave `Manufacturer 2` / `Manufacturer Part 2` blank**, so do +not do this unless the engineer explicitly asks you to find a second source. It's documented +here so it can be switched on later without redesigning anything. + +If asked to find a second source, it is an **equivalent part from a different manufacturer** that +could drop into the design unchanged: + +1. **Pull the original's form-fit-function specs** from the datasheet — capacitance, voltage, + tolerance, dielectric/temp class, package (for a cap); resistance, power, tolerance, package + (for a resistor); and so on per type. +2. **Search for an equivalent from a *different* manufacturer** (distributor/parametric + cross-reference, "equivalent to ``", or another reputable maker's matching series). It + must match **every** form-fit-function spec and be **at least as good** on tolerance / + voltage / temperature — never worse. +3. **Fill** `Manufacturer 2` = its maker, `Manufacturer Part 2` = its exact MPN. + +Two hard rules if this is ever used: **never invent an MPN** (only a part number you verified +exists; else leave blank), and **always flag the chosen second source for the engineer to +confirm** with the specs you compared. + +## Building the parameter set + +Collect the values into a `params.json` (same spirit as `part.json`). `component` is the +symbol's Library Ref (from `altium_refs.py`); omit it to apply to every component in the lib. + +```json +{ + "component": "CC0402FRNPO9BN120", + "comment": "CC0402FRNPO9BN120", + "parameters": { + "Value": "12pF_0402", + "Manufacturer Part": "CC0402FRNPO9BN120", + "Manufacturer": "YAGEO", + "Manufacturer Part 2": "0402N120F500CT", + "Manufacturer 2": "Walsin Tech Corp", + "Process": "Reflow", + "Vecmocon Part Code": "VECESC2421", + "Operating Temperature": "-55 °C to +125 °C", + "Tolerance": "1%", + "Datasheet": "https://www.lcsc.com/datasheet/C326662.pdf", + "ROHS": "Yes" + } +} +``` + +## Writing them into the symbol + +Write the parameters straight into the `.SchLib`, producing a new file: + +```bash +python scripts/schlib_write.py --schlib .SchLib --params params.json --out .SchLib +``` + +`params.json` may carry a `"remove"` list (defaults to `["Manufacturer_Name", +"Manufacturer_Part_Number"]`); those Ultra-Librarian defaults are stripped and the SOP params +added. The script targets the component named in `"component"` (its Library Ref / storage name), +or every component if omitted, and self-checks the output re-opens as a valid OLE. Deliver the +resulting `.SchLib`, and 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): + +```bash +python scripts/altium_params.py script --params params.json --out apply_params.pas +``` + +Then in Altium: open the `.SchLib`, **DXP → Run Script… → ApplyParameters**, review, **Save to +Server**. diff --git a/scripts/altium_params.py b/scripts/altium_params.py new file mode 100644 index 0000000..037eab6 --- /dev/null +++ b/scripts/altium_params.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +"""Generate an Altium DelphiScript that stamps the mandatory parameters (SOP §5) onto a +schematic-library symbol, from a params.json the skill builds off the datasheet. + +Why a script instead of editing the .SchLib directly: a .SchLib is an OLE compound-binary that +we can read but cannot safely grow from outside Altium (adding parameter records means +rewriting Altium's own format). So the skill produces the parameter *values* from the datasheet +and emits a small script the engineer runs inside Altium (DXP -> Run Script -> ApplyParameters), +which adds/updates exactly these parameters on the component and sets the Comment to the MPN. +See references/schlib_parameters.md for the parameter set and each value's source. + +params.json: + { + "component": "CC0402FRNPO9BN120", # Library Ref of the target symbol; omit -> all comps + "comment": "CC0402FRNPO9BN120", # sets the Comment field (usually the MPN) + "parameters": {"Value": "12pF_0402", "Manufacturer": "YAGEO", ...} + } + +Usage: + python altium_params.py script --params params.json --out apply_params.pas + python altium_params.py script --params params.json # prints to stdout + +The generated .pas is plain text; validate its first run on a new Altium version, since the +scripting API can differ slightly between releases. +""" +import argparse, json, sys + + +def _q(s): + """Quote a value as a DelphiScript single-quoted string literal (doubling embedded quotes). + None/blank -> ''. Non-strings are stringified.""" + if s is None: + s = "" + return "'" + str(s).replace("'", "''") + "'" + + +PROLOGUE = """{ Auto-generated by library-manager (altium_params.py). + Stamps the SOP mandatory parameters onto a schematic-library symbol. + HOW TO RUN: open the target .SchLib in Altium, then DXP -> Run Script -> pick this file + -> run 'ApplyParameters'. It adds each parameter if missing, updates it if present, sets the + Comment field, hides blank-valued parameters, and leaves the library open for you to review + and Save to Server (with a revision note, per the SOP). } + +Var + Lib : ISch_Lib; + LibIter : ISch_Iterator; + Comp : ISch_Component; + TargetRef: String; + CommentText : String; + +Procedure SetParam(AComp : ISch_Component; AName : String; AValue : String); +Var + PIter : ISch_Iterator; + P : ISch_Parameter; + Found : ISch_Parameter; +Begin + Found := Nil; + PIter := AComp.SchIterator_Create; + PIter.AddFilter_ObjectSet(MkSet(eParameter)); + Try + P := PIter.FirstSchObject; + While P <> Nil Do + Begin + If P.Name = AName Then Found := P; + P := PIter.NextSchObject; + End; + Finally + AComp.SchIterator_Destroy(PIter); + End; + + If Found <> Nil Then + Begin + Found.Text := AValue; + Found.IsHidden := (AValue = ''); + End + Else + Begin + Found := SchServer.SchObjectFactory(eParameter, eCreate_GlobalCopy); + Found.Name := AName; + Found.Text := AValue; + Found.IsHidden := (AValue = ''); + AComp.AddSchObject(Found); + End; +End; + +Procedure ApplyToComponent(AComp : ISch_Component); +Begin + SchServer.RobotManager.SendMessage(AComp.I_ObjectAddress, c_BroadCast, + SCHM_BeginModify, c_NoEventData); + Try + If CommentText <> '' Then AComp.Comment.Text := CommentText; +""" + +EPILOGUE = """ Finally + SchServer.RobotManager.SendMessage(AComp.I_ObjectAddress, c_BroadCast, + SCHM_EndModify, c_NoEventData); + End; +End; + +Procedure ApplyParameters; +Begin + If SchServer = Nil Then + Begin + ShowMessage('Schematic server not available.'); + Exit; + End; + Lib := SchServer.GetCurrentSchDocument; + If Lib = Nil Then + Begin + ShowMessage('Open the target .SchLib first.'); + Exit; + End; + If Lib.ObjectID <> eSchLib Then + Begin + ShowMessage('The active document is not a .SchLib.'); + Exit; + End; + + LibIter := Lib.SchLibIterator_Create; + LibIter.AddFilter_ObjectSet(MkSet(eSchComponent)); + Try + Comp := LibIter.FirstSchObject; + While Comp <> Nil Do + Begin + If (TargetRef = '') Or (Comp.LibReference = TargetRef) Then + ApplyToComponent(Comp); + Comp := LibIter.NextSchObject; + End; + Finally + Lib.SchLibIterator_Destroy(LibIter); + End; + + Lib.GraphicallyInvalidate; + ShowMessage('Parameters applied. Review the component, then Save to Server.'); +End; +""" + + +def generate(params): + comp = params.get("component", "") or "" + comment = params.get("comment", "") or "" + fields = params.get("parameters", {}) or {} + + setparam_calls = "\n".join( + f" SetParam(AComp, {_q(name)}, {_q(value)});" for name, value in fields.items() + ) + + # TargetRef / CommentText are globals set by InitTargets, called first in ApplyParameters. + init_block = ( + "Procedure InitTargets;\n" + "Begin\n" + f" TargetRef := {_q(comp)};\n" + f" CommentText := {_q(comment)};\n" + "End;\n\n" + ) + + body = PROLOGUE + setparam_calls + "\n" + EPILOGUE + # Ensure InitTargets is called first inside ApplyParameters. + body = body.replace("Procedure ApplyParameters;\nBegin\n", + "Procedure ApplyParameters;\nBegin\n InitTargets;\n") + # Place InitTargets AFTER the global Var block (Pascal: declarations before use), i.e. just + # before the first procedure. The Var block lives at the top of PROLOGUE. + return body.replace("Procedure SetParam(AComp", + init_block + "Procedure SetParam(AComp", 1) + + +def main(): + ap = argparse.ArgumentParser() + sub = ap.add_subparsers(dest="cmd", required=True) + s = sub.add_parser("script", help="emit the Altium DelphiScript from a params.json") + s.add_argument("--params", required=True) + s.add_argument("--out", help="write the .pas here (default: stdout)") + a = ap.parse_args() + + params = json.load(open(a.params, encoding="utf-8")) + pas = generate(params) + if a.out: + with open(a.out, "w", encoding="utf-8") as f: + f.write(pas) + n = len(params.get("parameters", {})) + print(f"wrote {a.out} ({n} parameters" + + (f", component '{params['component']}'" if params.get("component") else ", all components") + + ")") + else: + sys.stdout.write(pas) + + +if __name__ == "__main__": + main() diff --git a/scripts/schlib_write.py b/scripts/schlib_write.py new file mode 100644 index 0000000..ffc952b --- /dev/null +++ b/scripts/schlib_write.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python3 +"""Write Vecmocon's mandatory parameters directly INTO an Altium schematic-library (.SchLib) +file, in pure Python — no Altium needed. + +Reading a value out of a .SchLib is a plain stream read (see altium_refs.py). Writing is harder: +parameters live as text records inside the component's ``Data`` stream, and adding/removing them +changes that stream's size, which means rebuilding the whole OLE compound container around it. +This script does exactly that while **preserving every original byte except the Data stream(s) +it must change** — all other streams (FileHeader, Storage, …), the directory tree, the Altium +CLSIDs and timestamps are copied through untouched, and only the size/offset fields that must +move are recomputed. + +What it does to the target component's Data stream: + - removes the Ultra-Librarian default params ``Manufacturer_Name`` / ``Manufacturer_Part_Number`` + (they duplicate the SOP ``Manufacturer`` / ``Manufacturer Part`` — override with --keep or a + "remove" list in params.json), + - adds/updates the SOP parameters from params.json (see references/schlib_parameters.md), + - leaves pins, graphics, the Comment and all other records exactly as they were. + +Usage: + python schlib_write.py --schlib IN.SchLib --params params.json --out OUT.SchLib + +params.json (same shape altium_params.py uses): + {"component":"JMK105BJ105KV-F", # LibRef / component-storage name; omit -> all comps + "parameters":{"Value":"1u","Manufacturer":"Taiyo Yuden", ...}, + "remove":["Manufacturer_Name","Manufacturer_Part_Number"]} # optional; this is the default + +IMPORTANT: this writes Altium's own binary format from outside Altium. It is validated to +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. +""" +import argparse, json, struct, sys, hashlib +import olefile + +FREESECT=0xFFFFFFFF; ENDOFCHAIN=0xFFFFFFFE; FATSECT=0xFFFFFFFD +SEC=512; MINI=64; CUTOFF=4096 +DEFAULT_REMOVE=["Manufacturer_Name", "Manufacturer_Part_Number"] + + +def le16(b,o): return struct.unpack('path, sid->content-bytes).""" + raw=open(path,'rb').read() + dir_start=le32(raw,48) + difat=[le32(raw,76+4*i) for i in range(109)] + fat=[] + for s in difat: + if s>=0xFFFFFFFC: continue + sec=raw[512+s*SEC:512+(s+1)*SEC] + fat+=[struct.unpack('=2 else '' + paths={} + def walk(sid, prefix): + if sid>=0xFFFFFFFC or sid>=len(entries): return + e=entries[sid] + walk(le32(e,68), prefix) # left + nm=name_of(e); paths[sid]=prefix+[nm] + if e[66]==1: # storage -> recurse children + walk(le32(e,76), prefix+[nm]) + walk(le32(e,72), prefix) # right + walk(le32(entries[0],76), []) # from root's child + + ole=olefile.OleFileIO(path) + bypath={'/'.join(e):ole.openstream(e).read() for e in ole.listdir(streams=True)} + ole.close() + content={sid:bypath['/'.join(p)] for sid,p in paths.items() if '/'.join(p) in bypath} + return entries, paths, content + + +# ----------------------------------------------------------------- edit a Data stream + +def _uid(name): + h=''.join(c for c in hashlib.md5(name.encode()).hexdigest().upper() if c.isalpha()) + return (h+"ABCDEFGH")[:8] + + +def _param_record(idx, name, value): + s=(f"|RECORD=41|IndexInSheet={idx}|OwnerPartId=1|Justification=4|FontID=2|IsHidden=T" + f"|Text={value}|Name={name}|UniqueID={_uid(name)}") + payload=s.encode('latin-1')+b'\x00' + return struct.pack('len(data): break + payload=data[i+4:i+4+L] + if payload[-1:]!=b'\x00' or not payload[:8].startswith(b'|RECORD='): + break + recs.append(data[i:i+4+L]); i+=4+L + return recs, data[i:] + + +def _rec_name(block): + t=block[4:-1].decode('latin-1') + return t.split('|Name=')[1].split('|')[0] if '|Name=' in t else None + + +def edit_data(data, params, remove): + """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.""" + leading, tail = _leading_text_records(data) + sop_names=set(params) + kept=[] + for blk in leading: + nm=_rec_name(blk) + if nm is not None and (nm in remove or nm in sop_names): + continue # drop UL duplicates + stale SOP copies + kept.append(blk) + added=[_param_record(20+i, nm, val) for i,(nm,val) in enumerate(params.items())] + return b''.join(kept)+b''.join(added)+tail + + +# ----------------------------------------------------------------- rebuild the container + +def _set(e, off, val, n): e[off:off+n]=val.to_bytes(n,'little') + +def rebuild(entries, content): + """Rebuild an OLE compound file from raw directory entries + {sid: content}. Small streams + (<4096) go in the mini-stream; big streams get their own FAT sectors. Directory-entry bytes + are preserved; only each entry's start-sector and size are patched.""" + minis=[sid for sid,c in content.items() if len(c)=CUTOFF] + + ministream=b''; mini_start={} + for sid in minis: + mini_start[sid]=len(ministream)//MINI + ministream+=pad(content[sid],MINI) + n_mini=len(ministream)//MINI + minifat=[ENDOFCHAIN]*n_mini + for sid in minis: + st=mini_start[sid]; cnt=(len(content[sid])+MINI-1)//MINI or 1 + for k in range(cnt-1): minifat[st+k]=st+k+1 + minifat_bytes=pad(b''.join(struct.pack(' n_fat*SEC: n_fat+=1 + fat_secs=list(range(cur,cur+n_fat)); total=cur+n_fat + if n_fat>109: sys.exit("too many FAT sectors for a header-only DIFAT (file unexpectedly large)") + + # patch directory entries: start-sector + size + for sid in bigs: _set(entries[sid],116,big_secs[sid][0][0],4); entries[sid][120:128]=struct.pack('=2 and p[-1].lower()=="data" and sid in content: + if component is None or p[-2]==component: + targets.append(sid) + if not targets: + sys.exit(f"no component Data stream found" + (f" for '{component}'" if component else "")) + for sid in targets: + content[sid]=edit_data(content[sid], fields, remove) + + blob=rebuild(entries, content) + open(out,'wb').write(blob) + + # self-check: re-open and confirm it's a valid OLE with the params present + if not olefile.isOleFile(out): + sys.exit("ERROR: rebuilt file is not a valid OLE — aborting") + ole=olefile.OleFileIO(out) + ok=True + for e in ole.listdir(streams=True): + if e[-1].lower()=="data": + t=ole.openstream(e).read().decode('latin-1','ignore') + for nm in fields: + if f"|Name={nm}|" not in t and f"|Name={nm}\x00" not in t and f"Name={nm}" not in t: + ok=False + ole.close() + n_comp=len(targets) + print(f"wrote {out} ({len(fields)} params into {n_comp} component(s); removed {remove}) " + f"{'[self-check OK]' if ok else '[WARN: verify params]'}") + + +def main(): + ap=argparse.ArgumentParser() + ap.add_argument("--schlib", required=True) + ap.add_argument("--params", required=True) + ap.add_argument("--out", required=True) + a=ap.parse_args() + write_params(a.schlib, json.load(open(a.params, encoding="utf-8")), a.out) + + +if __name__ == "__main__": + main()