Add direct .SchLib parameter writer (schlib_write.py): write SOP mandatory params into symbol, Value=value-only, remove UL Manufacturer_Name/Part_Number; docs updated
parent
1e3c22f34f
commit
d39b0792a9
55
SKILL.md
55
SKILL.md
|
|
@ -290,6 +290,12 @@ python scripts/fill_templates.py part.json \
|
||||||
--template assets/template/template.xlsx --dest <stage>/<tag>/ --design design.json
|
--template assets/template/template.xlsx --dest <stage>/<tag>/ --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
|
### 7. Assemble the part folder
|
||||||
|
|
||||||
The staging folder `<tag>/` should now hold the four files: the per-part `<tag>.xlsx`, the
|
The staging folder `<tag>/` should now hold the four files: the per-part `<tag>.xlsx`, the
|
||||||
|
|
@ -306,6 +312,47 @@ This places the folder at `components/<Class>/<tag>/` — creating the Class fol
|
||||||
doesn't exist yet, or pushing into it if it does — and commits and pushes. Confirm to the
|
doesn't exist yet, or pushing into it if it does — and commits and pushes. Confirm to the
|
||||||
user where it landed.
|
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 <in>.SchLib --params params.json --out <stage>/<tag>/<sym>.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
|
## Per-typeid versioning
|
||||||
|
|
||||||
Versioning is **per typeid**, not global. Each typeid carries its own `template_version` and
|
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:
|
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 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.
|
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/template/versions.json` — per-typeid `template_version` + `skill_version`.
|
||||||
- `assets/CHANGELOG.xlsx` — global version/parameter changelog (created on first add;
|
- `assets/CHANGELOG.xlsx` — global version/parameter changelog (created on first add;
|
||||||
merged into the skill repo's copy in Gitea by `push-skill`).
|
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
|
- `scripts/append_parameter.py` — append parameter(s) to a typeid, bump its versions, write
|
||||||
the changelog.
|
the changelog.
|
||||||
- `scripts/altium_refs.py` — read Library/Footprint Ref from `.SchLib`/`.PcbLib`.
|
- `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
|
- `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),
|
update), `checkout`, `list-type`, `place-part`, `commit-push`, `push-part` (library repo),
|
||||||
and `push-skill` (skill repo: token-blanked push + append-only changelog merge).
|
and `push-skill` (skill repo: token-blanked push + append-only changelog merge).
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -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 `<MPN>`", 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 <in>.SchLib --params params.json --out <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**.
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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('<H',b[o:o+2])[0]
|
||||||
|
def le32(b,o): return struct.unpack('<I',b[o:o+4])[0]
|
||||||
|
def pad(b,n): return b+b'\x00'*((-len(b))%n)
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------- read the container
|
||||||
|
|
||||||
|
def read_container(path):
|
||||||
|
"""Return (raw_dir_entries[list of bytearray(128)], sid->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('<I',sec[k:k+4])[0] for k in range(0,SEC,4)]
|
||||||
|
def chain(start):
|
||||||
|
out=[]; s=start
|
||||||
|
while s not in (ENDOFCHAIN,FREESECT) and s<len(fat):
|
||||||
|
out.append(s); s=fat[s]
|
||||||
|
return out
|
||||||
|
dirbytes=b''.join(raw[512+s*SEC:512+(s+1)*SEC] for s in chain(dir_start))
|
||||||
|
entries=[bytearray(dirbytes[i:i+128]) for i in range(0,len(dirbytes),128)]
|
||||||
|
|
||||||
|
def name_of(e):
|
||||||
|
nlen=le16(e,64)
|
||||||
|
return e[0:max(0,nlen-2)].decode('utf-16-le') if nlen>=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('<I',len(payload))+payload
|
||||||
|
|
||||||
|
|
||||||
|
def _leading_text_records(data):
|
||||||
|
"""Split Data into (list of leading text-record byte-blocks, tail bytes). The leading run is
|
||||||
|
the contiguous |RECORD=…| text records at the start (RECORD=1 + the parameter block); it
|
||||||
|
stops at the first non-text record (the binary pin records), which — with everything after —
|
||||||
|
is returned untouched as the tail."""
|
||||||
|
recs=[]; i=0
|
||||||
|
while i+4<=len(data):
|
||||||
|
L=struct.unpack('<I',data[i:i+4])[0]
|
||||||
|
if L==0 or i+4+L>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]
|
||||||
|
bigs =[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('<I',x) for x in minifat),SEC) if n_mini else b''
|
||||||
|
n_mf=len(minifat_bytes)//SEC
|
||||||
|
|
||||||
|
# sector layout: [big streams][ministream][miniFAT][directory][FAT]
|
||||||
|
ms_p=pad(ministream,SEC); n_ms=len(ms_p)//SEC
|
||||||
|
big_secs={}; cur=0
|
||||||
|
for sid in bigs:
|
||||||
|
b=pad(content[sid],SEC); k=len(b)//SEC
|
||||||
|
big_secs[sid]=(list(range(cur,cur+k)), b); cur+=k
|
||||||
|
ms_secs=list(range(cur,cur+n_ms)); cur+=n_ms
|
||||||
|
mf_secs=list(range(cur,cur+n_mf)); cur+=n_mf
|
||||||
|
dir_bytes=pad(b''.join(entries),SEC); n_dir=len(dir_bytes)//SEC
|
||||||
|
dir_secs=list(range(cur,cur+n_dir)); cur+=n_dir
|
||||||
|
data_secs=cur
|
||||||
|
n_fat=1
|
||||||
|
while (data_secs+n_fat)*4 > 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('<Q',len(content[sid]))
|
||||||
|
for sid in minis: _set(entries[sid],116,mini_start[sid],4); entries[sid][120:128]=struct.pack('<Q',len(content[sid]))
|
||||||
|
entries[0][120:128]=struct.pack('<Q',len(ministream)) # root: ministream size
|
||||||
|
_set(entries[0],116, ms_secs[0] if n_ms else ENDOFCHAIN, 4)
|
||||||
|
dir_bytes=pad(b''.join(entries),SEC)
|
||||||
|
|
||||||
|
# FAT
|
||||||
|
FAT=[FREESECT]*total
|
||||||
|
def ch(secs):
|
||||||
|
for k in range(len(secs)-1): FAT[secs[k]]=secs[k+1]
|
||||||
|
if secs: FAT[secs[-1]]=ENDOFCHAIN
|
||||||
|
for sid in bigs: ch(big_secs[sid][0])
|
||||||
|
ch(ms_secs); ch(mf_secs); ch(dir_secs)
|
||||||
|
for s in fat_secs: FAT[s]=FATSECT
|
||||||
|
fat_bytes=pad(b''.join(struct.pack('<I',x) for x in FAT),SEC)
|
||||||
|
|
||||||
|
# header
|
||||||
|
h=bytearray(512)
|
||||||
|
h[0:8]=bytes([0xD0,0xCF,0x11,0xE0,0xA1,0xB1,0x1A,0xE1])
|
||||||
|
h[24:26]=struct.pack('<H',0x003E); h[26:28]=struct.pack('<H',3)
|
||||||
|
h[28:30]=struct.pack('<H',0xFFFE); h[30:32]=struct.pack('<H',9); h[32:34]=struct.pack('<H',6)
|
||||||
|
h[44:48]=struct.pack('<I',n_fat)
|
||||||
|
h[48:52]=struct.pack('<I',dir_secs[0])
|
||||||
|
h[56:60]=struct.pack('<I',CUTOFF)
|
||||||
|
h[60:64]=struct.pack('<I',mf_secs[0] if n_mf else ENDOFCHAIN)
|
||||||
|
h[64:68]=struct.pack('<I',n_mf)
|
||||||
|
h[68:72]=struct.pack('<I',ENDOFCHAIN)
|
||||||
|
for i in range(109):
|
||||||
|
h[76+4*i:80+4*i]=struct.pack('<I', fat_secs[i] if i<n_fat else FREESECT)
|
||||||
|
|
||||||
|
out=bytearray(h)
|
||||||
|
for sid in bigs: out+=big_secs[sid][1]
|
||||||
|
out+=ms_p+minifat_bytes+dir_bytes+fat_bytes
|
||||||
|
return bytes(out)
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------- driver
|
||||||
|
|
||||||
|
def write_params(schlib, params_json, out):
|
||||||
|
component=params_json.get("component") or None
|
||||||
|
fields=params_json.get("parameters", {}) or {}
|
||||||
|
remove=params_json.get("remove", DEFAULT_REMOVE)
|
||||||
|
entries, paths, content = read_container(schlib)
|
||||||
|
|
||||||
|
# target Data stream sid(s): a stream named 'Data' whose parent storage == component (or all)
|
||||||
|
targets=[]
|
||||||
|
for sid,p in paths.items():
|
||||||
|
if len(p)>=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()
|
||||||
Loading…
Reference in New Issue