Add Altium 365 Part Request (web browser) submission workflow + reference for non-admin end task

main
admin 2026-07-15 05:33:42 +00:00
parent 265027ebab
commit 6a2f6a553b
No known key found for this signature in database
7 changed files with 205 additions and 126 deletions

View File

@ -286,25 +286,11 @@ 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 write the symbol's Altium parameters Once you have the symbol and its Library Ref, you can also produce the **mandatory symbol
onto the `.SchLib`. **House rule: the symbol carries _every_ parameter the workbook was filled parameters** the SOP requires on the `.SchLib` (Manufacturer, Manufacturer Part, Value,
with from the datasheet — not just the fixed SOP set.** Build the parameter set straight from the Tolerance, Operating Temperature, RoHS, Datasheet, Process, Vecmocon Part Code, …) — see
finished `<tag>.xlsx` and write it in: *Mandatory symbol parameters* below. This is optional per run but is how the datasheet values
land in the Altium symbol's properties.
```bash
python scripts/schlib_params_from_xlsx.py --xlsx <stage>/<tag>/<tag>.xlsx \
--component <LibraryRef> --set "Process=<Reflow|Wave>" --set "Datasheet=<url-or-doc-ref>" \
--out params.json
python scripts/schlib_write.py --schlib <in>.SchLib --params params.json --out <stage>/<tag>/<sym>.SchLib
```
`schlib_params_from_xlsx.py` keeps every filled datasheet column (exact header as the parameter
name), drops the identity/version/model-link columns, renames `Rohs compliance → ROHS` and
`Operating Temp(°C) → Operating Temperature`, and merges in the SOP-only fields (`Manufacturer
Part` = MPN automatically; `Process` / `Datasheet` / `Value` / `Vecmocon Part Code` via `--set`).
See *Mandatory symbol parameters* below and `references/schlib_parameters.md` for the full method
(incl. the ANSI glyph transliteration, e.g. `Ω → Ohm`). This is how the datasheet values land in
the Altium symbol's properties.
### 7. Assemble the part folder ### 7. Assemble the part folder
@ -327,11 +313,8 @@ user where it landed.
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 — `Manufacturer`, `Manufacturer Part`, `Value`, `Tolerance`, `Operating properties — `Manufacturer`, `Manufacturer Part`, `Value`, `Tolerance`, `Operating
Temperature`, `ROHS`, `Datasheet`, `Process`, `Vecmocon Part Code`, and the two second-source Temperature`, `ROHS`, `Datasheet`, `Process`, `Vecmocon Part Code`, and the two second-source
fields — with the **Comment** set to the MPN. **On top of that fixed minimum, the symbol also fields — with the **Comment** set to the MPN. The skill can stamp these onto the symbol from
carries every other parameter the part's workbook was filled with from the datasheet** (e.g. a the datasheet.
CMC's `Rated Current(A)`, `DC Resistance(mΩ)`, `Package`, `ESD Withstand Voltage(kV)`): the
Altium properties mirror the `<tag>.xlsx` row. The skill can stamp all of these onto the symbol
from the workbook + datasheet.
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
@ -357,19 +340,16 @@ 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`.
Build the `params.json` from the finished workbook (so it carries all the filled datasheet Write the **full parameter set** — the typeid template's engineering columns **plus** the SOP
parameters), then write it into the symbol: params above (see `references/schlib_parameters.md`). Collect your filled values into a
`params.json` and pass `--typeid` so the writer guarantees every template column is present
(blank where the datasheet is silent):
```bash ```bash
python scripts/schlib_params_from_xlsx.py --xlsx <stage>/<tag>/<tag>.xlsx \ python scripts/schlib_write.py --schlib <in>.SchLib --params params.json \
--component <LibraryRef> --set "Process=<Reflow|Wave>" --set "Datasheet=<url-or-doc-ref>" \ --out <stage>/<tag>/<sym>.SchLib --typeid <TYPEID>
[--set "Value=<value>"] [--set "Vecmocon Part Code=<code>"] --out params.json
python scripts/schlib_write.py --schlib <in>.SchLib --params params.json --out <stage>/<tag>/<sym>.SchLib
``` ```
(For a bare `.SchLib`-only task with no workbook, you can still hand-write `params.json` — the
shape is in `references/schlib_parameters.md`.)
Deliver the resulting `.SchLib`; the engineer opens it in Altium once to confirm it loads, then Deliver the resulting `.SchLib`; the engineer opens it in Altium once to confirm it loads, then
**Saves to Server** with a revision note. The full parameter set, each value's source, the **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 `params.json` shape (incl. the `remove` list), and the mini-stream size caveat are in
@ -378,6 +358,24 @@ round-trip, fall back to `scripts/altium_params.py` (emits an Altium DXP script
parameters from inside Altium). Always have the engineer confirm the file opens in Altium — the 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. skill writes Altium's own binary format, so Altium is the final validator.
## Submitting to Altium 365 as Part Requests (web)
If the org's central library is a managed **Altium 365 Workspace** (not the Gitea repos), the
skill's end task can submit each finished component as a **Part Request** through the Workspace
web UI, using browser automation (Claude-in-Chrome) in the operator's own signed-in Chrome — no
API token, no admin rights. A librarian then approves each request into the library.
This runs over **every component processed in the run**: the skill writes a `part_requests.json`
manifest (one entry per component — manufacturer, MPN, Description, component type, the full
parameter set, and the local paths to its `.SchLib`/`.PcbLib`/datasheet), then the browser step
loops it, filling and submitting the form for each. On the **first** component, fill everything
and stop at Save for the operator to review; once confirmed, Save and loop the rest, logging each
Request Id. Full field mapping, prerequisites, and the exact browser steps are in
`references/part_request_web.md` — read it before driving the browser.
Because the files upload from local disk, commit each component's `.SchLib`/`.PcbLib`/datasheet
to the operator's machine (device bridge) first, and put those local paths in the manifest.
## 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
@ -537,6 +535,9 @@ plain flat push, but it does not merge the changelog or blank the token, so pref
- `references/schlib_parameters.md` — the SOP **mandatory symbol parameters** (§5) for the - `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 `.SchLib`: the parameter set, where each value comes from, and how the generated Altium script
stamps them onto the symbol. stamps them onto the symbol.
- `references/part_request_web.md` — submitting finished components to a managed Altium 365
Workspace as **Part Requests** via browser automation (no token/admin): prerequisites, the
per-component field mapping, the `part_requests.json` manifest, and the looped browser steps.
- `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`).
@ -547,14 +548,9 @@ 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_params_from_xlsx.py` — build the symbol `params.json` from the finished - `scripts/schlib_write.py` — write the SOP mandatory parameters **directly into a `.SchLib`**
per-part `<tag>.xlsx`, so the `.SchLib` carries **every filled datasheet parameter** (not just (pure-Python OLE rebuild; removes the Ultra-Librarian `Manufacturer_Name` /
the SOP minimum); skips identity/version/model-link columns, renames a couple to their SOP `Manufacturer_Part_Number` defaults). Primary path; see `references/schlib_parameters.md`.
names, and merges in the SOP-only fields (`Manufacturer Part`, `Process`, `Datasheet`, …).
- `scripts/schlib_write.py` — write the parameters **directly into a `.SchLib`** (pure-Python OLE
rebuild; removes the Ultra-Librarian `Manufacturer_Name` / `Manufacturer_Part_Number` defaults;
transliterates non-ANSI unit glyphs, e.g. `Ω → Ohm`). Primary path; see
`references/schlib_parameters.md`.
- `scripts/altium_params.py` — fallback: generate an Altium DelphiScript that stamps the same - `scripts/altium_params.py` — fallback: generate an Altium DelphiScript that stamps the same
parameters onto a `.SchLib` from inside Altium (DXP → Run Script). 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

Binary file not shown.

Binary file not shown.

View File

@ -88,8 +88,8 @@
"template_version": 1 "template_version": 1
}, },
"CMC": { "CMC": {
"skill_version": 2, "skill_version": 1,
"template_version": 2 "template_version": 1
}, },
"CMP": { "CMP": {
"skill_version": 1, "skill_version": 1,

View File

@ -0,0 +1,82 @@
# Submitting components as Altium 365 Part Requests (web browser)
When the central library is a managed **Altium 365 Workspace** and the operator is **not an
admin**, the skill's end task can be to submit each finished component as a **Part Request**
through the Workspace's web UI, driven by browser automation (Claude-in-Chrome). A librarian
then approves each request into the managed library. This runs in the operator's **own Chrome,
using their existing Altium 365 login**, so it needs **no API token and no admin rights** — it
just does what the engineer would do by hand, for every component in the run.
This is the browser alternative to the headless API push (`altium365_push.py`, if a token is
ever available) and to the Gitea push. Use whichever matches how the org consumes the library.
## Prerequisites (each run)
- **Chrome open** with the Claude-in-Chrome extension enabled, and **site permission granted**
for the Workspace domain (e.g. `vecmocon.altium365.com`).
- **Signed in** to the Altium 365 Workspace as any member with rights to create Part Requests.
- **The component files on the LOCAL machine** — the browser uploads attachments from local
disk, so each component's `.SchLib` (with parameters written), `.PcbLib`, and datasheet must
exist on the operator's computer. If the skill produced them in the cloud, commit them to the
device first (device bridge) into a known folder, and put those local paths in the manifest.
- **The manifest** of components to submit this run (below).
- **Decisions the operator confirms once**: the default **Assign to** (the librarian/group), the
**Component Type** mapping for each typeid, and an optional **Required By** date.
## Per-component field mapping (the Part Request form)
| Form field | Value (from the skill's per-part data) |
|---|---|
| Manufacturer | `Manufacturer` param (e.g. `Taiyo Yuden`) |
| Manufacturer Part Numbers | `Manufacturer Part` param (the MPN) |
| Description | `Description` param (SOP format, e.g. `CHIP_CAP_1uF_6.3v_±10%_0402_x5r`) |
| Component Type | the typeid mapped to the Workspace's matching component type (confirm once per typeid) |
| State | leave `Opened: New` |
| Required By Date | optional org default |
| Assign to | the configured librarian / group |
| Parameters → Add | every parameter from the full set (name + value) |
| Attachments | the `.SchLib`, `.PcbLib`, and datasheet files |
| Parts List → Add | optional: add the MPN as a part choice so the librarian's mapping is pre-seeded |
## The manifest
The skill writes one `part_requests.json` per run listing every component it processed, so the
browser step can loop without re-deriving anything:
```json
{"requests":[
{"manufacturer":"Taiyo Yuden","mpn":"JMK105BJ105KV-F",
"description":"CHIP_CAP_1uF_6.3v_±10%_0402_x5r","component_type":"Capacitor",
"parameters":{"Value":"1u","Voltage(V)":"6.3","Tolerance":"±10%","...":"..."},
"files":["C:\\...\\JMK105BJ105KV-F.SchLib","C:\\...\\<footprint>.PcbLib","C:\\...\\<mpn>.pdf"],
"assignee":"<librarian>","required_by":""}
]}
```
Build it from each component's `params.json` plus the local file paths (after committing files
to the device).
## Browser procedure (looped per component)
Start the browser session with `tabs_context_mcp`, then for each entry in the manifest:
1. Navigate to **Library → Part Requests → new request**.
2. Fill **Manufacturer**, **Manufacturer Part Numbers**, **Description**.
3. Choose **Component Type**; set **Assign to**; optional **Required By Date**. Leave State as
`Opened: New`.
4. **Parameters → Add**: add each parameter name + value.
5. **Attachments** (Choose file / drop): upload the `.SchLib`, `.PcbLib`, and datasheet.
6. Review, then **Save**. Record the auto-assigned **Request Id**.
7. Move to the next entry.
## Safety and auditing
- On the **first component of a run**, fill everything and **stop at Save** for the operator to
eyeball the mapping. Once they confirm it looks right, Save it and loop the rest unattended.
- **Log every submitted Request Id** (and any component that failed) so the run is auditable and
re-runnable — never silently skip a component.
- Browser automation follows the live UI. If a field, dropdown option, or a popup doesn't match
what's expected, **pause and ask** rather than guessing — a wrong Component Type or a
half-filled request is worse than one clarifying question.
- Don't trigger native file-dialog blocking: use the extension's file-upload path for
attachments, not an OS dialog.

View File

@ -5,50 +5,6 @@ parameters in its component properties (the panel shown in Altium: *Properties
This file defines that set, where each value comes from, and how the skill stamps them onto the This file defines that set, where each value comes from, and how the skill stamps them onto the
`.SchLib` symbol. `.SchLib` symbol.
## The symbol mirrors the workbook (fill everything the datasheet gave)
**House rule: the symbol's Altium properties must carry _every_ parameter that was filled into
the part's per-typeid workbook from the datasheet — not just the fixed SOP §5 set.** So a CMC
symbol also gets `Rated Current(A)`, `Rated Voltage(V)`, `DC Resistance(mΩ)`, `Package`,
`ESD Withstand Voltage(kV)`, … — whatever that typeid's sheet holds and the datasheet filled.
The SOP set below is the **minimum**; the workbook is the **source of truth** for the rest.
Build the parameter set straight from the finished `<tag>.xlsx` with
`scripts/schlib_params_from_xlsx.py`, which reads the one data row and keeps every **non-empty**
column, then hand its output to `schlib_write.py`:
```bash
python scripts/schlib_params_from_xlsx.py --xlsx <stage>/<tag>/<tag>.xlsx \
--component <LibraryRef> \
--set "Process=Reflow" --set "Datasheet=<url-or-doc-ref>" \
[--set "Value=<value>"] [--set "Vecmocon Part Code=<code>"] \
--out params.json
python scripts/schlib_write.py --schlib <in>.SchLib --params params.json --out <stage>/<tag>/<sym>.SchLib
```
What the builder does:
- **Keeps every filled datasheet column** as a symbol parameter, using the **exact sheet header
as the parameter name** (e.g. `Rated Current(A)`) so the symbol and the workbook stay
traceably identical. Empty columns are left out (the SOP hides blank parameters).
- **Never writes** the identity / versioning / model-link columns — `MPN_make_type`,
`Skill Version`, `Template Version`, `Library Ref/Path`, `Footprint Ref/Path` (Library Ref is
the symbol's own name and the footprint is the linked PCB model, not a text property).
- **Renames** the two columns whose Altium/SOP name differs — `Rohs compliance → ROHS`,
`Operating Temp(°C) → Operating Temperature` — value copied through unchanged.
- **Merges in the SOP-only fields the sheet doesn't hold**: `Manufacturer Part` (= the MPN,
recovered from the tag) is added automatically; pass `Process`, `Datasheet`, and (if known)
`Value` / `Vecmocon Part Code` via `--set` or a `--sop` JSON. A non-empty override wins over a
sheet value; an empty one is ignored.
Glyph note: Altium stores parameter text as single-byte ANSI, so `schlib_write.py` transliterates
the few unit glyphs that aren't representable — the ohm sign `Ω → Ohm` (so `DC Resistance(mΩ)`
lands as `DC Resistance(mOhm)`) and a Greek micro `μ → u`; `±`, `°` and the latin-1 micro sign
pass through unchanged. So a couple of symbol parameter names are the ASCII form of the sheet
header — expected, not a mismatch.
The rest of this file describes the SOP §5 minimum set and where each value comes from.
How the parameters get in: the skill writes them **directly into the `.SchLib` in pure Python** 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 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 `Data` stream while preserving every other byte (all other streams, the directory tree, the
@ -137,24 +93,35 @@ symbol's Library Ref (from `altium_refs.py`); omit it to apply to every componen
} }
``` ```
## The full parameter set (template + SOP)
Every `.SchLib` should carry the **complete** parameter set for its part: the **typeid
template's engineering columns** (all columns of that typeid's `template.xlsx` sheet except the
internal bookkeeping ones — the tag `MPN_make_type`, `Skill Version`, `Template Version`, and the
four `Library/Footprint Ref/Path` columns) **plus** the mandatory SOP params above. So a CER
(ceramic MLCC) symbol gets `Capacitance(uF)`, `Tolerance`, `Voltage(V)`,
`Dielectric(temp. Coefficient)`, `Operating Temp(°C)`, `Max operating temp(°C)`, `Package`,
`Description`, `Manufacturer` from the template, alongside `Value`, `Manufacturer Part`,
`Process`, `Vecmocon Part Code`, `ROHS`, `Datasheet`, and the second-source fields. Fill each
from the datasheet; leave blank what the datasheet doesn't state.
## Writing them into the symbol ## Writing them into the symbol
Write the parameters straight into the `.SchLib`, producing a new file: Write the parameters straight into the `.SchLib`, producing a new file. Pass `--typeid` so the
writer guarantees the whole template column set is present (blank where you didn't supply a
value) — this is what keeps every symbol's parameter set complete and consistent:
```bash ```bash
python scripts/schlib_write.py --schlib <in>.SchLib --params params.json --out <out>.SchLib python scripts/schlib_write.py --schlib <in>.SchLib --params params.json --out <out>.SchLib --typeid <TYPEID>
``` ```
`params.json` may carry a `"remove"` list (defaults to `["Manufacturer_Name", `params.json` carries your filled values (and may include a `"remove"` list — defaults to
"Manufacturer_Part_Number"]`); those Ultra-Librarian defaults are stripped and the SOP params `["Manufacturer_Name", "Manufacturer_Part_Number"]`, the Ultra-Librarian duplicates that get
added. The script targets the component named in `"component"` (its Library Ref / storage name), stripped). The script targets the component named in `"component"` (its Library Ref / storage
or every component if omitted, and self-checks the output re-opens as a valid OLE. Deliver the name), or every component if omitted, and self-checks the output re-opens as a valid OLE. It
resulting `.SchLib`, and have the engineer open it in Altium once to confirm it loads, then Save handles any parameter-set size — small sets stay in Altium's mini-stream, larger ones are written
to Server with a revision note per the SOP. as a regular stream automatically. Deliver the resulting `.SchLib`; have the engineer open it in
Altium once to confirm it loads, then Save to Server with a revision note per the SOP.
Scope note: the direct writer keeps a component's `Data` under Altium's 4096-byte mini-stream
threshold in the common case; a very large parameter set (or an extremely long datasheet URL)
can push it past that, at which point fall back to the `altium_params.py` DXP-script path.
Fallback (apply from inside Altium): Fallback (apply from inside Altium):

View File

@ -29,7 +29,7 @@ IMPORTANT: this writes Altium's own binary format from outside Altium. It is val
re-open as a well-formed OLE with every other stream byte-identical, but ALWAYS open the result re-open as a well-formed OLE with every other stream byte-identical, but ALWAYS open the result
in Altium once to confirm it loads before relying on it. in Altium once to confirm it loads before relying on it.
""" """
import argparse, json, struct, sys, hashlib, unicodedata import argparse, json, os, struct, sys, hashlib
import olefile import olefile
FREESECT=0xFFFFFFFF; ENDOFCHAIN=0xFFFFFFFE; FATSECT=0xFFFFFFFD FREESECT=0xFFFFFFFF; ENDOFCHAIN=0xFFFFFFFE; FATSECT=0xFFFFFFFD
@ -42,6 +42,26 @@ def le32(b,o): return struct.unpack('<I',b[o:o+4])[0]
def pad(b,n): return b+b'\x00'*((-len(b))%n) def pad(b,n): return b+b'\x00'*((-len(b))%n)
# ----------------------------------------------------------------- typeid template columns
# Template columns that are internal library bookkeeping, NOT symbol parameters.
NON_PARAM_COLS = {"MPN_make_type", "Skill Version", "Template Version",
"Library Ref", "Library Path", "Footprint Ref", "Footprint Path"}
def template_param_names(template_path, typeid):
"""The symbol-parameter columns for a typeid = every column on that typeid's template sheet
EXCEPT the internal bookkeeping ones (the tag, the two version columns, and the four design
Ref/Path columns). These are the engineering parameters that belong on the symbol."""
import openpyxl
wb = openpyxl.load_workbook(template_path, read_only=True)
if typeid not in wb.sheetnames:
raise SystemExit(f"no template sheet for typeid '{typeid}'")
ws = wb[typeid]
return [ws.cell(1, c).value for c in range(1, ws.max_column + 1)
if ws.cell(1, c).value and ws.cell(1, c).value not in NON_PARAM_COLS]
# ----------------------------------------------------------------- read the container # ----------------------------------------------------------------- read the container
def read_container(path): def read_container(path):
@ -90,30 +110,10 @@ def _uid(name):
return (h+"ABCDEFGH")[:8] return (h+"ABCDEFGH")[:8]
# Altium stores parameter text as single-byte ANSI (≈ latin-1). A few unit glyphs that show up
# in the Excel column names (the ohm sign Ω, a Greek micro µ) aren't representable there, so map
# them to ASCII before encoding; ±, °, and the latin-1 micro sign (0xB5) pass through unchanged.
_GLYPH={'Ω':'Ohm','':'Ohm','μ':'u'}
def _enc(s):
s=''.join(_GLYPH.get(c,c) for c in s)
try:
return s.encode('latin-1')
except UnicodeEncodeError: # transliterate any remaining non-ANSI glyph
out=[]
for c in s:
try:
c.encode('latin-1'); out.append(c)
except UnicodeEncodeError:
d=unicodedata.normalize('NFKD',c).encode('ascii','ignore').decode('ascii')
out.append(d or '_')
return ''.join(out).encode('latin-1')
def _param_record(idx, name, value): def _param_record(idx, name, value):
s=(f"|RECORD=41|IndexInSheet={idx}|OwnerPartId=1|Justification=4|FontID=2|IsHidden=T" s=(f"|RECORD=41|IndexInSheet={idx}|OwnerPartId=1|Justification=4|FontID=2|IsHidden=T"
f"|Text={value}|Name={name}|UniqueID={_uid(name)}") f"|Text={value}|Name={name}|UniqueID={_uid(name)}")
payload=_enc(s)+b'\x00' payload=s.encode('latin-1')+b'\x00'
return struct.pack('<I',len(payload))+payload return struct.pack('<I',len(payload))+payload
@ -138,16 +138,34 @@ def _rec_name(block):
return t.split('|Name=')[1].split('|')[0] if '|Name=' in t else None return t.split('|Name=')[1].split('|')[0] if '|Name=' in t else None
def _patch_field(block, field, value):
"""Replace |field=...| inside a length-prefixed text record, re-framing its 4-byte length.
Used to set the component's ComponentDescription in the RECORD=1 header."""
import re
text = block[4:-1].decode('latin-1')
if f"|{field}=" in text:
text = re.sub(rf"\|{re.escape(field)}=[^|]*", f"|{field}={value}", text, count=1)
elif text.startswith("|RECORD="):
text = text + f"|{field}={value}"
payload = text.encode('latin-1') + b'\x00'
return struct.pack('<I', len(payload)) + payload
def edit_data(data, params, remove): def edit_data(data, params, remove):
"""Return a new Data stream: drop `remove` params, drop any SOP-name params (re-added """Return a new Data stream: drop `remove` params, drop any SOP-name params (re-added
fresh), keep everything else, then append the SOP params. Pins/graphics/tail untouched.""" fresh), keep everything else, then append the SOP params. Also mirror the `Description`
parameter into the component's ComponentDescription field (the Altium 'Description' shown in
the component properties). Pins/graphics/tail untouched."""
leading, tail = _leading_text_records(data) leading, tail = _leading_text_records(data)
sop_names=set(params) sop_names=set(params)
desc = params.get("Description")
kept=[] kept=[]
for blk in leading: for blk in leading:
nm=_rec_name(blk) nm=_rec_name(blk)
if nm is not None and (nm in remove or nm in sop_names): if nm is not None and (nm in remove or nm in sop_names):
continue # drop UL duplicates + stale SOP copies continue # drop UL duplicates + stale SOP copies
if desc is not None and blk[4:-1].startswith(b"|RECORD=1|"):
blk = _patch_field(blk, "ComponentDescription", desc) # component Description field
kept.append(blk) kept.append(blk)
added=[_param_record(20+i, nm, val) for i,(nm,val) in enumerate(params.items())] added=[_param_record(20+i, nm, val) for i,(nm,val) in enumerate(params.items())]
return b''.join(kept)+b''.join(added)+tail return b''.join(kept)+b''.join(added)+tail
@ -231,10 +249,20 @@ def rebuild(entries, content):
# ----------------------------------------------------------------- driver # ----------------------------------------------------------------- driver
def write_params(schlib, params_json, out): def write_params(schlib, params_json, out, typeid=None, template=None):
component=params_json.get("component") or None component=params_json.get("component") or None
fields=params_json.get("parameters", {}) or {} fields=dict(params_json.get("parameters", {}) or {})
remove=params_json.get("remove", DEFAULT_REMOVE) remove=params_json.get("remove", DEFAULT_REMOVE)
# If a typeid+template are given, guarantee the FULL template parameter set is written:
# every engineering column for that typeid becomes a symbol parameter (value from the
# params if provided, else blank). This is what makes every .SchLib carry the complete,
# consistent parameter set the template defines — not just whatever was hand-listed.
typeid = typeid or params_json.get("typeid")
template = template or params_json.get("template")
if typeid and template:
for name in template_param_names(template, typeid):
fields.setdefault(name, "")
entries, paths, content = read_container(schlib) entries, paths, content = read_container(schlib)
# target Data stream sid(s): a stream named 'Data' whose parent storage == component (or all) # target Data stream sid(s): a stream named 'Data' whose parent storage == component (or all)
@ -260,7 +288,6 @@ def write_params(schlib, params_json, out):
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:
nm=_enc(nm).decode('latin-1') # compare against how the name was actually written
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
ole.close() ole.close()
@ -274,8 +301,15 @@ def main():
ap.add_argument("--schlib", required=True) ap.add_argument("--schlib", required=True)
ap.add_argument("--params", required=True) ap.add_argument("--params", required=True)
ap.add_argument("--out", required=True) ap.add_argument("--out", required=True)
ap.add_argument("--typeid", help="component typeid; with --template, writes that typeid's "
"full template parameter set (blank where not provided)")
ap.add_argument("--template", help="path to template.xlsx (defaults to the skill's)")
a=ap.parse_args() a=ap.parse_args()
write_params(a.schlib, json.load(open(a.params, encoding="utf-8")), a.out) template = a.template
if a.typeid and not template:
template = os.path.join(os.path.dirname(__file__), "..", "assets", "template", "template.xlsx")
write_params(a.schlib, json.load(open(a.params, encoding="utf-8")), a.out,
typeid=a.typeid, template=template)
if __name__ == "__main__": if __name__ == "__main__":