library-manager: add MPN column to all templates (no version bump); connected-folder copy; datasheet Gitea link
parent
e460f2d085
commit
f56fe9d0a3
99
SKILL.md
99
SKILL.md
|
|
@ -55,6 +55,27 @@ and report it. A symbol that still holds Ultra-Librarian defaults is an incomple
|
||||||
|
|
||||||
Every other flow keeps the normal interactive asks below.
|
Every other flow keeps the normal interactive asks below.
|
||||||
|
|
||||||
|
## Keep a copy in the connected folder — always, after every part push
|
||||||
|
|
||||||
|
So the operator ends up with the parts they worked on **on their own machine** — without
|
||||||
|
having to go into the admin Gitea to fetch them — the skill drops a copy of each pushed part
|
||||||
|
folder into the **connected session folder**. At the start of a Cowork session the operator
|
||||||
|
connects a folder; that folder is the destination. If no folder was explicitly connected, use
|
||||||
|
the session's output directory `/mnt/user-data/outputs/` (create it if missing) so the copy
|
||||||
|
still lands somewhere the operator can retrieve.
|
||||||
|
|
||||||
|
The rule is: **immediately after any successful part push to Gitea**, copy that part's
|
||||||
|
`<MPN>_<make>_<typeid>/` folder — with its files intact (`<tag>.xlsx`, datasheet, `.SchLib`,
|
||||||
|
`.PcbLib`) — into the connected folder. This is automatic and never a question, exactly like
|
||||||
|
the push itself. Details and the exact command are in *Copy the pushed folder to the connected
|
||||||
|
folder* below.
|
||||||
|
|
||||||
|
Two boundaries: (1) copy **only after the push succeeds** — a failed push must not leave a
|
||||||
|
misleading copy on the operator's machine; (2) this applies to **part** pushes only
|
||||||
|
(`push-part`, and the `commit-push` used by updates, backfill, and the `.SchLib` fill-and-push).
|
||||||
|
**Skill-repo** pushes (`push-skill`, template/changelog changes) involve no part folder and are
|
||||||
|
excluded.
|
||||||
|
|
||||||
## Inputs
|
## Inputs
|
||||||
|
|
||||||
- **A datasheet PDF whose filename is the MPN** (e.g. `BAT46WJ.pdf`). If it's a series
|
- **A datasheet PDF whose filename is the MPN** (e.g. `BAT46WJ.pdf`). If it's a series
|
||||||
|
|
@ -256,7 +277,9 @@ python scripts/fill_templates.py part.json \
|
||||||
|
|
||||||
This writes `<tag>.xlsx` with column A = the tag, **Skill Version (col B)** and **Template
|
This writes `<tag>.xlsx` with column A = the tag, **Skill Version (col B)** and **Template
|
||||||
Version (col C)** stamped from this typeid's current versions, and the four design columns
|
Version (col C)** stamped from this typeid's current versions, and the four design columns
|
||||||
left blank for now.
|
left blank for now. Every sheet also carries an **`MPN` column** (positioned just before
|
||||||
|
`Manufacturer`) that `fill_templates` populates with the part's bare MPN — it's filled from
|
||||||
|
`part["mpn"]` automatically, so no extra input is needed.
|
||||||
|
|
||||||
The part workbook has up to **two sheets**:
|
The part workbook has up to **two sheets**:
|
||||||
|
|
||||||
|
|
@ -318,11 +341,9 @@ new-part run. Do not ask "would you like me to fill the symbol parameters?", do
|
||||||
a suggestion, and do not defer it to a later turn. Just do it, then report that it's done.
|
a suggestion, and do not defer it to a later turn. Just do it, then report that it's done.
|
||||||
|
|
||||||
The SOP requires the fixed parameter set (Manufacturer, Manufacturer Part, Value, Tolerance,
|
The SOP requires the fixed parameter set (Manufacturer, Manufacturer Part, Value, Tolerance,
|
||||||
Operating Temperature, RoHS, Datasheet, Process, Vecmocon Part Code, …) on every symbol, **plus
|
Operating Temperature, RoHS, Datasheet, Process, Vecmocon Part Code, …) on every symbol, so a
|
||||||
the symbol's Description**, so a part folder whose `.SchLib` has not been filled is **incomplete
|
part folder whose `.SchLib` has not been filled is **incomplete and must not be pushed**. Build
|
||||||
and must not be pushed**. Build the part's `params.json` from the values the engineer just
|
the part's `params.json` from the values the engineer just verified in step 5 and run:
|
||||||
verified in step 5 — including `"description"`, set to the **same** strict `Class_TYPEID` string
|
|
||||||
you put in the part's Excel — and run:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python scripts/schlib_write.py \
|
python scripts/schlib_write.py \
|
||||||
|
|
@ -331,6 +352,12 @@ python scripts/schlib_write.py \
|
||||||
--out <stage>/<tag>/<symbol_name>.SchLib
|
--out <stage>/<tag>/<symbol_name>.SchLib
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`params.json` must carry **`description`** and **`comment`** alongside `parameters`. The
|
||||||
|
`description` is the strict `Class_TYPEID_…` string you already built for the workbook — reuse
|
||||||
|
the *exact same string* so the symbol's ComponentDescription and the Excel `Description` column
|
||||||
|
never diverge. The `comment` is the MPN. If the script warns that `description` is missing, treat
|
||||||
|
that as an error: fix `params.json` and re-run before pushing.
|
||||||
|
|
||||||
See *Mandatory symbol parameters* below for the full parameter set, the value-shorthand rules,
|
See *Mandatory symbol parameters* below for the full parameter set, the value-shorthand rules,
|
||||||
and the Ultra-Librarian default-stripping behaviour — read `references/schlib_parameters.md`
|
and the Ultra-Librarian default-stripping behaviour — read `references/schlib_parameters.md`
|
||||||
before building the parameter set. The values are already verified at this point, so this step
|
before building the parameter set. The values are already verified at this point, so this step
|
||||||
|
|
@ -354,22 +381,38 @@ python scripts/gitea_components.py push-part --folder <stage>/<tag> --typeid <ty
|
||||||
|
|
||||||
This places the folder at `components/<Class>/<tag>/` — creating the Class folder if it
|
This places the folder at `components/<Class>/<tag>/` — creating the Class folder if it
|
||||||
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. **Then copy the folder to the connected folder** (next section).
|
||||||
|
|
||||||
|
### Copy the pushed folder to the connected folder — automatic, after every part push
|
||||||
|
|
||||||
|
Right after the push above **succeeds**, copy the just-pushed `<MPN>_<make>_<typeid>/` folder
|
||||||
|
into the connected session folder so the operator has a local copy without touching Gitea. Use
|
||||||
|
the folder the operator connected at the start of the session; if none was connected, fall back
|
||||||
|
to `/mnt/user-data/outputs/`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DEST="${CONNECTED_FOLDER:-/mnt/user-data/outputs}" # the folder connected this session
|
||||||
|
mkdir -p "$DEST/<tag>"
|
||||||
|
cp -r <stage>/<tag>/. "$DEST/<tag>/" # xlsx, datasheet, .SchLib, .PcbLib
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy the folder as `<tag>/` (i.e. `<MPN>_<make>_<typeid>/`) — just the part folder, not the
|
||||||
|
`<Class>/` parent — so the connected folder simply accumulates one folder per part the operator
|
||||||
|
worked on. If that folder already exists there (a re-run or an update), refresh it so the local
|
||||||
|
copy matches what was just pushed. Then tell the user the copy is in the connected folder.
|
||||||
|
|
||||||
|
This copy step runs after **every** part push, including the `commit-push` used by *Updating an
|
||||||
|
existing part*, *Backfilling existing parts*, and the `.SchLib` *fill-and-push* — run the same
|
||||||
|
three lines against that part's folder once its push succeeds. It does **not** run for
|
||||||
|
`push-skill` (skill-repo/template/changelog pushes carry no part folder).
|
||||||
|
|
||||||
## Mandatory symbol parameters (.SchLib)
|
## Mandatory symbol parameters (.SchLib)
|
||||||
|
|
||||||
The SOP (§5) requires every schematic symbol to carry a fixed parameter set in its Altium
|
The SOP (§5) requires every schematic symbol to carry a fixed parameter set in its Altium
|
||||||
properties — `Component Type`, `Manufacturer`, `Manufacturer Part`, `Value`, `Tolerance`,
|
properties — `Component Type`, `Manufacturer`, `Manufacturer Part`, `Value`, `Tolerance`,
|
||||||
`Operating Temperature`, `ROHS`, `Datasheet`, `Process`, `Vecmocon Part Code`, and the two
|
`Operating Temperature`, `ROHS`, `Datasheet`, `Process`, `Vecmocon Part Code`, and the two
|
||||||
second-source fields — with the **Comment** set to the MPN and the **Description** set to the
|
second-source fields — with the **Comment** set to the MPN. The skill can stamp these onto the
|
||||||
part's strict `Class_TYPEID` string. The skill stamps all of these onto the symbol from the
|
symbol from the datasheet.
|
||||||
datasheet in a single pass.
|
|
||||||
|
|
||||||
**Description is written too, always.** It isn't a parameter — it's the `ComponentDescription`
|
|
||||||
field in the symbol's header, which Ultra-Librarian ships as the placeholder text `Description`.
|
|
||||||
So it must be written explicitly or the symbol shows the literal word "Description" in Altium
|
|
||||||
even when every parameter is right. Pass `"description"` in `params.json` (it falls back to
|
|
||||||
`parameters.Description`), and use the **same** string as the part's Excel so the two agree.
|
|
||||||
|
|
||||||
This runs **as its own task too**, not only inside new-part creation: whenever the user hands
|
This runs **as its own task too**, not only inside new-part creation: whenever the user hands
|
||||||
over one or more `.SchLib` files and wants their parameters filled/updated (e.g. "\schlib", "add
|
over one or more `.SchLib` files and wants their parameters filled/updated (e.g. "\schlib", "add
|
||||||
|
|
@ -388,7 +431,12 @@ UL `Component_Type` — replaced by Vecmocon's own spaced `Component Type`). Kin
|
||||||
- **Read from the datasheet** (don't just echo given text — open the PDF and fill the real,
|
- **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
|
verified values): `Value` = the **value only** in shorthand (e.g. `1u`, `12p`, `10k` — no
|
||||||
package), plus `Manufacturer`, `Manufacturer Part`, `Operating Temperature`, `Tolerance`,
|
package), plus `Manufacturer`, `Manufacturer Part`, `Operating Temperature`, `Tolerance`,
|
||||||
`ROHS`, `Datasheet`, and `Process` by inference from the package.
|
`ROHS`, and `Process` by inference from the package.
|
||||||
|
- **Derived link** — `Datasheet` = the **Gitea URL to the pushed `<MPN>_data.<ext>`**, so the
|
||||||
|
symbol clicks straight through to the exact datasheet in the library. Build it from `GIT_HOST`
|
||||||
|
+ `LIBRARY_REPO` and the part's Class/tag
|
||||||
|
(`https://<GIT_HOST>/<LIBRARY_REPO>/raw/branch/main/<Class>/<tag>/<MPN>_data.<ext>`); the full
|
||||||
|
rule is in `references/schlib_parameters.md`.
|
||||||
- **Leave blank for now** — `Manufacturer 2` / `Manufacturer Part 2` (the second source). Don't
|
- **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
|
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
|
cross-reference search to find a second source is documented in
|
||||||
|
|
@ -402,8 +450,7 @@ 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` — with `"description"` alongside `"parameters"` — and
|
Collect the values into a `params.json` and write them into the symbol:
|
||||||
write them into the symbol:
|
|
||||||
|
|
||||||
```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
|
||||||
|
|
@ -464,7 +511,8 @@ the symbol clearly isn't this component, stop and flag it rather than pushing th
|
||||||
Pushing without asking is consistent with the skill's standing rule that pushing is automatic;
|
Pushing without asking is consistent with the skill's standing rule that pushing is automatic;
|
||||||
what's new here is that the Excel fill is automatic too. Tell the user which parameters you
|
what's new here is that the Excel fill is automatic too. Tell the user which parameters you
|
||||||
filled, that the symbol was updated, and where in Gitea it landed. (A part-data fill isn't a
|
filled, that the symbol was updated, and where in Gitea it landed. (A part-data fill isn't a
|
||||||
template change, so nothing version-bumps and the changelog is untouched.)
|
template change, so nothing version-bumps and the changelog is untouched.) **Then copy the
|
||||||
|
part folder to the connected folder** — see *Copy the pushed folder to the connected folder*.
|
||||||
|
|
||||||
## Per-typeid versioning
|
## Per-typeid versioning
|
||||||
|
|
||||||
|
|
@ -477,6 +525,14 @@ into that typeid's rows (cols B and C), so a row always records the template/ski
|
||||||
was built against. `append_parameter.py` does the bump; `common.py` is the single source for
|
was built against. `append_parameter.py` does the bump; `common.py` is the single source for
|
||||||
reading and writing these numbers.
|
reading and writing these numbers.
|
||||||
|
|
||||||
|
**One deliberate exception already applied:** the `MPN` column (before `Manufacturer`) was
|
||||||
|
added to **every** typeid sheet as a one-off structural change **without** bumping any
|
||||||
|
`template_version`/`skill_version` and **without** a changelog row — it's a bookkeeping column,
|
||||||
|
not a spec parameter, and versions were intentionally left where they were. Don't treat the
|
||||||
|
existing sheets' MPN column as a version discrepancy to "correct." (This no-bump behaviour is
|
||||||
|
specific to that one change; genuine parameter additions still go through `append_parameter.py`
|
||||||
|
and bump normally.)
|
||||||
|
|
||||||
## The changelog
|
## The changelog
|
||||||
|
|
||||||
`append_parameter.py` maintains one **global** changelog as an Excel workbook at
|
`append_parameter.py` maintains one **global** changelog as an Excel workbook at
|
||||||
|
|
@ -519,6 +575,9 @@ and tell the user the previous sheets were updated:
|
||||||
python scripts/gitea_components.py commit-push --root work/ --message "backfill <param> into <typeid>"
|
python scripts/gitea_components.py commit-push --root work/ --message "backfill <param> into <typeid>"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Then copy each rebuilt part folder into the connected folder (see *Copy the pushed folder to
|
||||||
|
the connected folder*) so the operator's local copies reflect the backfill too.
|
||||||
|
|
||||||
## Updating an existing part
|
## Updating an existing part
|
||||||
|
|
||||||
When the duplicate check finds the part (or the user asks to revise one that's already in
|
When the duplicate check finds the part (or the user asks to revise one that's already in
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -22,16 +22,9 @@ inside Altium; keep it as a fallback if a particular file doesn't round-trip.)
|
||||||
|
|
||||||
Use these exact Altium parameter names (they must match the symbol, per the SOP screenshot).
|
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
|
The **Comment** field is set to the MPN (SOP §4), and the **Description** field is the strict
|
||||||
string from `references/description_format.md`.
|
string from `references/description_format.md` — the *same* string as the part's Excel
|
||||||
|
`Description` column. Both are written by `schlib_write.py` from the `comment` and `description`
|
||||||
**The Description is not a parameter record.** It is the `ComponentDescription` field of the
|
keys of `params.json`; neither is optional.
|
||||||
component's `RECORD=1` header — the box shown at *Properties → General → Description*, above the
|
|
||||||
Parameters table. Ultra-Librarian ships it as the literal placeholder text `Description`, so a
|
|
||||||
symbol whose parameters are all correctly filled will *still* show the word "Description" in
|
|
||||||
Altium unless that header field is rewritten. `schlib_write.py` now sets it in the same pass as
|
|
||||||
the parameters: pass `"description"` in `params.json`, or let it fall back to your
|
|
||||||
`parameters.Description`. Always give it the same strict `Class_TYPEID` string you wrote into the
|
|
||||||
part's Excel, so the symbol and the workbook agree.
|
|
||||||
|
|
||||||
| Parameter | Source | Notes |
|
| Parameter | Source | Notes |
|
||||||
|-----------|--------|-------|
|
|-----------|--------|-------|
|
||||||
|
|
@ -45,16 +38,33 @@ part's Excel, so the symbol and the workbook agree.
|
||||||
| `Vecmocon Part Code` | engineer | internal code (e.g. `VECESC2421`) — **not on the datasheet**; ask |
|
| `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` |
|
| `Operating Temperature` | datasheet | full range, e.g. `-55 °C to +125 °C` |
|
||||||
| `Tolerance` | datasheet | e.g. `1%` (or `±1%`) |
|
| `Tolerance` | datasheet | e.g. `1%` (or `±1%`) |
|
||||||
| `Datasheet` | datasheet | source URL or document reference (SOP marks this optional) |
|
| `Datasheet` | derived | **Gitea link to the pushed `<MPN>_data.<ext>`** — see below (SOP marks this optional) |
|
||||||
| `ROHS` | datasheet | RoHS compliance, `Yes`/`No` |
|
| `ROHS` | datasheet | RoHS compliance, `Yes`/`No` |
|
||||||
|
|
||||||
**Read the datasheet-sourced values from the actual datasheet — don't echo whatever text was
|
**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`,
|
handed to you.** Open the PDF, find each real value (`Value`, `Manufacturer Part`,
|
||||||
`Manufacturer`, `Operating Temperature`, `Tolerance`, `Datasheet`, `ROHS`, and `Process` by
|
`Manufacturer`, `Operating Temperature`, `Tolerance`, `ROHS`, and `Process` by
|
||||||
inference from the package), and fill them verified. An honest blank beats a guess — the SOP
|
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
|
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.
|
purely internal and must come from the engineer: `Vecmocon Part Code` — ask for it.
|
||||||
|
|
||||||
|
**The `Datasheet` field is a link, not a value read off the PDF.** Point it at the datasheet
|
||||||
|
this skill pushes into the part's Gitea folder (`<MPN>_data.<ext>`), so opening the symbol in
|
||||||
|
Altium clicks straight through to the exact datasheet used. Build it from the config
|
||||||
|
(`GIT_HOST`, `LIBRARY_REPO` in `config/gitea.env`) and the part's Class + tag:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://<GIT_HOST>/<LIBRARY_REPO>/raw/branch/main/<Class>/<MPN>_<make>_<typeid>/<MPN>_data.<ext>
|
||||||
|
```
|
||||||
|
|
||||||
|
e.g. `https://gitea.vecmocon.com/nitishKumar/library/raw/branch/main/Diode/BAT46WJ_Nexperia_SCH/BAT46WJ_data.pdf`.
|
||||||
|
Use the `raw/branch/<default-branch>` form (not `src/…`) so the URL opens the PDF directly;
|
||||||
|
`<Class>` is `class_folder(typeid)` and `<MPN>_<make>_<typeid>` is the same tag the folder is
|
||||||
|
pushed under, so the link always resolves to the file that lands in Gitea. Fill this link even
|
||||||
|
on the first push of a new part — the push and the symbol write happen in the same run, so the
|
||||||
|
URL is valid as soon as the part folder lands. (If for some reason the datasheet isn't being
|
||||||
|
pushed to Gitea, leave `Datasheet` blank rather than writing a link that won't resolve.)
|
||||||
|
|
||||||
The second-source pair (`Manufacturer 2` / `Manufacturer Part 2`) is **left blank for now** —
|
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.
|
don't populate it by default. It simply stays hidden in Altium until someone fills it later.
|
||||||
|
|
||||||
|
|
@ -85,11 +95,22 @@ confirm** with the specs you compared.
|
||||||
Collect the values into a `params.json` (same spirit as `part.json`). `component` is the
|
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.
|
symbol's Library Ref (from `altium_refs.py`); omit it to apply to every component in the lib.
|
||||||
|
|
||||||
|
Two top-level fields sit alongside `parameters` and are **both mandatory**:
|
||||||
|
|
||||||
|
- **`description`** → the symbol's `ComponentDescription`. This must be **byte-identical to the
|
||||||
|
part's Excel `Description` column** — the same strict `Class_TYPEID_…` string built to
|
||||||
|
`references/description_format.md`. Build it once, use it in both places; never let the two
|
||||||
|
drift. A symbol must never ship carrying Altium's `No Description Available` placeholder.
|
||||||
|
- **`comment`** → the Comment record's Text, which per SOP §4 is always the exact MPN.
|
||||||
|
|
||||||
|
Both are patched **in place**, so the records keep their other fields (Comment keeps its
|
||||||
|
Location/Color/FontID); only the parameter block is dropped and re-added.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"component": "CC0402FRNPO9BN120",
|
"component": "CC0402FRNPO9BN120",
|
||||||
|
"description": "Capacitor_CER_12pF_50V_±1%_0402_c0g",
|
||||||
"comment": "CC0402FRNPO9BN120",
|
"comment": "CC0402FRNPO9BN120",
|
||||||
"description": "Capacitor_CER_12pF_50V_±1%_0402_NPO",
|
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Component Type": "Capacitor",
|
"Component Type": "Capacitor",
|
||||||
"Value": "12pF_0402",
|
"Value": "12pF_0402",
|
||||||
|
|
@ -101,7 +122,7 @@ symbol's Library Ref (from `altium_refs.py`); omit it to apply to every componen
|
||||||
"Vecmocon Part Code": "VECESC2421",
|
"Vecmocon Part Code": "VECESC2421",
|
||||||
"Operating Temperature": "-55 °C to +125 °C",
|
"Operating Temperature": "-55 °C to +125 °C",
|
||||||
"Tolerance": "1%",
|
"Tolerance": "1%",
|
||||||
"Datasheet": "https://www.lcsc.com/datasheet/C326662.pdf",
|
"Datasheet": "https://gitea.vecmocon.com/nitishKumar/library/raw/branch/main/Capacitor/CC0402FRNPO9BN120_YAGEO_CER/CC0402FRNPO9BN120_data.pdf",
|
||||||
"ROHS": "Yes"
|
"ROHS": "Yes"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -117,9 +138,16 @@ python scripts/schlib_write.py --schlib <in>.SchLib --params params.json --out <
|
||||||
|
|
||||||
`params.json` may carry a `"remove"` list (defaults to `["Manufacturer_Name",
|
`params.json` may carry a `"remove"` list (defaults to `["Manufacturer_Name",
|
||||||
"Manufacturer_Part_Number", "Copyright", "Component_Type"]`); those Ultra-Librarian defaults are
|
"Manufacturer_Part_Number", "Copyright", "Component_Type"]`); those Ultra-Librarian defaults are
|
||||||
stripped and the SOP params — including the spaced `Component Type` = Class — added. The script targets the component named in `"component"` (its Library Ref / storage name),
|
stripped and the SOP params — including the spaced `Component Type` = Class — added. The script
|
||||||
or every component if omitted, and self-checks the output re-opens as a valid OLE. Deliver the
|
also sets `ComponentDescription` from `"description"` and the Comment Text from `"comment"`, and
|
||||||
|
warns on stderr if `"description"` is missing — **treat that warning as an error** and re-run
|
||||||
|
with the Description rather than shipping the symbol. It 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 with the
|
||||||
|
params and Description present. Deliver the
|
||||||
resulting `.SchLib`, and have the engineer open it in Altium once to confirm it loads, then Save
|
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.
|
to Server with a revision note per the SOP.
|
||||||
|
|
||||||
|
The script needs the `olefile` package (`pip install olefile`) — its only non-stdlib
|
||||||
|
dependency. Install it if the import fails.
|
||||||
|
|
||||||
Scope note: the d
|
Scope note: the d
|
||||||
|
|
@ -95,6 +95,7 @@ def part_to_row(part, headers, design=None):
|
||||||
if key == norm(COL_TAG): row[h] = tag
|
if key == norm(COL_TAG): row[h] = tag
|
||||||
elif key == norm(COL_SKILL_VER): row[h] = sver
|
elif key == norm(COL_SKILL_VER): row[h] = sver
|
||||||
elif key == norm(COL_TEMPLATE_VER): row[h] = tver
|
elif key == norm(COL_TEMPLATE_VER): row[h] = tver
|
||||||
|
elif key == norm("MPN"): row[h] = part.get("mpn", vals.get("mpn", ""))
|
||||||
elif key == norm("Manufacturer"): row[h] = part.get("manufacturer", vals.get("manufacturer", ""))
|
elif key == norm("Manufacturer"): row[h] = part.get("manufacturer", vals.get("manufacturer", ""))
|
||||||
elif h in DESIGN_COLS: row[h] = dmap.get(key, vals.get(key, ""))
|
elif h in DESIGN_COLS: row[h] = dmap.get(key, vals.get(key, ""))
|
||||||
else: row[h] = vals.get(key, "")
|
else: row[h] = vals.get(key, "")
|
||||||
|
|
|
||||||
|
|
@ -16,28 +16,29 @@ What it does to the target component's Data stream:
|
||||||
and the UL ``Component_Type`` (Vecmocon adds its own spaced ``Component Type`` = Class instead);
|
and the UL ``Component_Type`` (Vecmocon adds its own spaced ``Component Type`` = Class instead);
|
||||||
override this default set with a "remove" list in params.json,
|
override this default set with a "remove" list in params.json,
|
||||||
- adds/updates the SOP parameters from params.json (see references/schlib_parameters.md),
|
- adds/updates the SOP parameters from params.json (see references/schlib_parameters.md),
|
||||||
- sets the component's ComponentDescription (the Description shown in Altium's Properties
|
- sets the component's ComponentDescription (RECORD=1) from params.json "description" — the
|
||||||
panel) — this lives in the RECORD=1 header, NOT in a parameter record, and Ultra-Librarian
|
strict Description string from references/description_format.md, the SAME text that goes in
|
||||||
ships it as the literal placeholder "Description", so it must be rewritten explicitly,
|
the part's Excel Description column. Mandatory: a symbol must never ship with Altium's
|
||||||
- leaves pins, graphics, the Comment and all other records exactly as they were.
|
"No Description Available" placeholder,
|
||||||
|
- sets the Comment record's Text from params.json "comment" (SOP §4: Comment = the MPN),
|
||||||
|
patching that record in place so its position/colour/font are preserved,
|
||||||
|
- leaves pins, graphics and all other records exactly as they were.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python schlib_write.py --schlib IN.SchLib --params params.json --out OUT.SchLib
|
python schlib_write.py --schlib IN.SchLib --params params.json --out OUT.SchLib
|
||||||
|
|
||||||
params.json (same shape altium_params.py uses):
|
params.json (same shape altium_params.py uses):
|
||||||
{"component":"JMK105BJ105KV-F", # LibRef / component-storage name; omit -> all comps
|
{"component":"JMK105BJ105KV-F", # LibRef / component-storage name; omit -> all comps
|
||||||
"description":"Capacitor_CER_1uF_35V_±10%_0402_x7r", # optional; else parameters.Description
|
"description":"Capacitor_CER_1uF_10v_±10%_0402_x5r", # -> ComponentDescription; == Excel Description
|
||||||
|
"comment":"JMK105BJ105KV-F", # -> Comment record Text (the MPN)
|
||||||
"parameters":{"Value":"1u","Manufacturer":"Taiyo Yuden","Component Type":"Capacitor", ...},
|
"parameters":{"Value":"1u","Manufacturer":"Taiyo Yuden","Component Type":"Capacitor", ...},
|
||||||
"remove":["Manufacturer_Name","Manufacturer_Part_Number","Copyright","Component_Type"]} # optional; default
|
"remove":["Manufacturer_Name","Manufacturer_Part_Number","Copyright","Component_Type"]} # optional; default
|
||||||
|
|
||||||
The Description is the same strict Class_TYPEID string written to the part's Excel
|
|
||||||
(references/description_format.md), so symbol and workbook always agree.
|
|
||||||
|
|
||||||
IMPORTANT: this writes Altium's own binary format from outside Altium. It is validated to
|
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
|
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, re, struct, sys, hashlib
|
||||||
import olefile
|
import olefile
|
||||||
|
|
||||||
FREESECT=0xFFFFFFFF; ENDOFCHAIN=0xFFFFFFFE; FATSECT=0xFFFFFFFD
|
FREESECT=0xFFFFFFFF; ENDOFCHAIN=0xFFFFFFFE; FATSECT=0xFFFFFFFD
|
||||||
|
|
@ -108,6 +109,21 @@ def _param_record(idx, name, value):
|
||||||
return struct.pack('<I',len(payload))+payload
|
return struct.pack('<I',len(payload))+payload
|
||||||
|
|
||||||
|
|
||||||
|
def _reframe(text):
|
||||||
|
"""Re-wrap an edited text record: Altium frames each as <4-byte LE length><payload><NUL>,
|
||||||
|
so any edit that changes the payload length must rewrite the prefix."""
|
||||||
|
payload=text.encode('latin-1')+b'\x00'
|
||||||
|
return struct.pack('<I',len(payload))+payload
|
||||||
|
|
||||||
|
|
||||||
|
def _sub_field(text, field, value):
|
||||||
|
"""Replace |<field>=... up to the next | (or end of record). Adds the field if absent."""
|
||||||
|
pat=re.compile(r'\|'+re.escape(field)+r'=[^|]*')
|
||||||
|
if pat.search(text):
|
||||||
|
return pat.sub('|'+field+'='+value, text, count=1)
|
||||||
|
return text+'|'+field+'='+value
|
||||||
|
|
||||||
|
|
||||||
def _leading_text_records(data):
|
def _leading_text_records(data):
|
||||||
"""Split Data into (list of leading text-record byte-blocks, tail bytes). The leading run is
|
"""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
|
the contiguous |RECORD=…| text records at the start (RECORD=1 + the parameter block); it
|
||||||
|
|
@ -129,40 +145,15 @@ 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 _text_block(text):
|
def edit_data(data, params, remove, description=None, comment=None):
|
||||||
"""Re-wrap an edited text record as its 4-byte length prefix + NUL-terminated payload."""
|
|
||||||
payload=text.encode('latin-1')+b'\x00'
|
|
||||||
return struct.pack('<I', len(payload))+payload
|
|
||||||
|
|
||||||
|
|
||||||
def _set_description(blk, description):
|
|
||||||
"""Set ComponentDescription on a RECORD=1 component-header block.
|
|
||||||
|
|
||||||
The symbol's Description (Altium: Properties -> General -> Description) is NOT a parameter
|
|
||||||
record — it's the ComponentDescription field of the RECORD=1 header. Ultra-Librarian ships
|
|
||||||
the literal placeholder `ComponentDescription=Description`, so unless this is rewritten the
|
|
||||||
symbol shows the word "Description" in Altium even after every SOP parameter is filled.
|
|
||||||
"""
|
|
||||||
text=blk[4:-1].decode('latin-1')
|
|
||||||
if not text.startswith('|RECORD=1|'):
|
|
||||||
return blk
|
|
||||||
if 'ComponentDescription=' in text:
|
|
||||||
parts=text.split('|')
|
|
||||||
for i,tok in enumerate(parts):
|
|
||||||
if tok.startswith('ComponentDescription='):
|
|
||||||
parts[i]='ComponentDescription='+description
|
|
||||||
text='|'.join(parts)
|
|
||||||
elif '|PartCount=' in text: # field absent entirely -> insert it
|
|
||||||
text=text.replace('|PartCount=', f'|ComponentDescription={description}|PartCount=', 1)
|
|
||||||
else:
|
|
||||||
text=text+f'|ComponentDescription={description}'
|
|
||||||
return _text_block(text)
|
|
||||||
|
|
||||||
|
|
||||||
def edit_data(data, params, remove, description=None):
|
|
||||||
"""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. When `description` is given, the
|
fresh), keep everything else, then append the SOP params. Pins/graphics/tail untouched.
|
||||||
RECORD=1 ComponentDescription field is rewritten too. Pins/graphics/tail untouched."""
|
|
||||||
|
`description` (when given) overwrites ComponentDescription on the RECORD=1 header — this is
|
||||||
|
the strict Description string, the same text as the part's Excel Description column, and is
|
||||||
|
required on every Vecmocon symbol. `comment` overwrites the Comment record's Text (the MPN).
|
||||||
|
Both are patched IN PLACE so the records keep their other fields (Comment keeps its
|
||||||
|
Location/Color/FontID), unlike the parameter block which is dropped and re-added."""
|
||||||
leading, tail = _leading_text_records(data)
|
leading, tail = _leading_text_records(data)
|
||||||
sop_names=set(params)
|
sop_names=set(params)
|
||||||
kept=[]
|
kept=[]
|
||||||
|
|
@ -170,8 +161,11 @@ def edit_data(data, params, remove, description=None):
|
||||||
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 description is not None and nm is None:
|
txt=blk[4:-1].decode('latin-1')
|
||||||
blk=_set_description(blk, description) # RECORD=1 header carries the Description
|
if description is not None and txt.startswith('|RECORD=1|'):
|
||||||
|
blk=_reframe(_sub_field(txt, 'ComponentDescription', description))
|
||||||
|
elif comment is not None and nm=='Comment' and '|RECORD=41|' in txt:
|
||||||
|
blk=_reframe(_sub_field(txt, 'Text', comment))
|
||||||
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
|
||||||
|
|
@ -259,9 +253,12 @@ def write_params(schlib, params_json, out):
|
||||||
component=params_json.get("component") or None
|
component=params_json.get("component") or None
|
||||||
fields=params_json.get("parameters", {}) or {}
|
fields=params_json.get("parameters", {}) or {}
|
||||||
remove=params_json.get("remove", DEFAULT_REMOVE)
|
remove=params_json.get("remove", DEFAULT_REMOVE)
|
||||||
# The symbol's Description: taken from "description", else the "Description" parameter if the
|
description=params_json.get("description") or None
|
||||||
# caller put it there (it's the same strict Class_TYPEID string that goes in the part's Excel).
|
comment=params_json.get("comment") or None
|
||||||
description=params_json.get("description") or fields.get("Description") or None
|
if not description:
|
||||||
|
print("WARNING: no 'description' in params.json — the symbol keeps whatever "
|
||||||
|
"ComponentDescription it has (often Altium's 'No Description Available'). "
|
||||||
|
"Vecmocon symbols must carry the Description; add it and re-run.", file=sys.stderr)
|
||||||
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)
|
||||||
|
|
@ -273,7 +270,7 @@ def write_params(schlib, params_json, out):
|
||||||
if not targets:
|
if not targets:
|
||||||
sys.exit(f"no component Data stream found" + (f" for '{component}'" if component else ""))
|
sys.exit(f"no component Data stream found" + (f" for '{component}'" if component else ""))
|
||||||
for sid in targets:
|
for sid in targets:
|
||||||
content[sid]=edit_data(content[sid], fields, remove, description)
|
content[sid]=edit_data(content[sid], fields, remove, description, comment)
|
||||||
|
|
||||||
blob=rebuild(entries, content)
|
blob=rebuild(entries, content)
|
||||||
open(out,'wb').write(blob)
|
open(out,'wb').write(blob)
|
||||||
|
|
@ -282,21 +279,24 @@ def write_params(schlib, params_json, out):
|
||||||
if not olefile.isOleFile(out):
|
if not olefile.isOleFile(out):
|
||||||
sys.exit("ERROR: rebuilt file is not a valid OLE — aborting")
|
sys.exit("ERROR: rebuilt file is not a valid OLE — aborting")
|
||||||
ole=olefile.OleFileIO(out)
|
ole=olefile.OleFileIO(out)
|
||||||
ok=True; desc_ok=(description is None)
|
ok=True
|
||||||
for e in ole.listdir(streams=True):
|
for e in ole.listdir(streams=True):
|
||||||
if e[-1].lower()=="data":
|
if e[-1].lower()=="data":
|
||||||
t=ole.openstream(e).read().decode('latin-1','ignore')
|
t=ole.openstream(e).read().decode('latin-1','ignore')
|
||||||
for nm in fields:
|
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:
|
if f"|Name={nm}|" not in t and f"|Name={nm}\x00" not in t and f"Name={nm}" not in t:
|
||||||
ok=False
|
ok=False
|
||||||
if description is not None and f"ComponentDescription={description}|" in t:
|
if description and f"|ComponentDescription={description}|" not in t \
|
||||||
desc_ok=True
|
and not t.split("|ComponentDescription=")[-1].startswith(description):
|
||||||
|
ok=False
|
||||||
ole.close()
|
ole.close()
|
||||||
n_comp=len(targets)
|
n_comp=len(targets)
|
||||||
desc_note=f"; description set" if description else "; no description given"
|
extra=[]
|
||||||
flag='[self-check OK]' if (ok and desc_ok) else '[WARN: verify params/description]'
|
if description: extra.append(f"description={description!r}")
|
||||||
|
if comment: extra.append(f"comment={comment!r}")
|
||||||
print(f"wrote {out} ({len(fields)} params into {n_comp} component(s); removed {remove}"
|
print(f"wrote {out} ({len(fields)} params into {n_comp} component(s); removed {remove}"
|
||||||
f"{desc_note}) {flag}")
|
+ ("; " + ", ".join(extra) if extra else "") + ") "
|
||||||
|
f"{'[self-check OK]' if ok else '[WARN: verify params]'}")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue