Add Altium 365 Part Request (web browser) submission workflow + reference for non-admin end task
parent
265027ebab
commit
6a2f6a553b
78
SKILL.md
78
SKILL.md
|
|
@ -286,25 +286,11 @@ python scripts/fill_templates.py part.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
|
||||
onto the `.SchLib`. **House rule: the symbol carries _every_ parameter the workbook was filled
|
||||
with from the datasheet — not just the fixed SOP set.** Build the parameter set straight from the
|
||||
finished `<tag>.xlsx` and write it in:
|
||||
|
||||
```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.
|
||||
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
|
||||
|
||||
|
|
@ -327,11 +313,8 @@ user where it landed.
|
|||
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. **On top of that fixed minimum, the symbol also
|
||||
carries every other parameter the part's workbook was filled with from the datasheet** (e.g. a
|
||||
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.
|
||||
fields — with the **Comment** set to the MPN. The skill can stamp these onto the symbol from
|
||||
the datasheet.
|
||||
|
||||
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
|
||||
|
|
@ -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
|
||||
`references/schlib_parameters.md`.
|
||||
|
||||
Build the `params.json` from the finished workbook (so it carries all the filled datasheet
|
||||
parameters), then write it into the symbol:
|
||||
Write the **full parameter set** — the typeid template's engineering columns **plus** the SOP
|
||||
params above (see `references/schlib_parameters.md`). Collect your filled values into a
|
||||
`params.json` and pass `--typeid` so the writer guarantees every template column is present
|
||||
(blank where the datasheet is silent):
|
||||
|
||||
```bash
|
||||
python scripts/schlib_params_from_xlsx.py --xlsx <stage>/<tag>/<tag>.xlsx \
|
||||
--component <LibraryRef> --set "Process=<Reflow|Wave>" --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
|
||||
python scripts/schlib_write.py --schlib <in>.SchLib --params params.json \
|
||||
--out <stage>/<tag>/<sym>.SchLib --typeid <TYPEID>
|
||||
```
|
||||
|
||||
(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
|
||||
**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
|
||||
|
|
@ -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
|
||||
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
|
||||
|
||||
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
|
||||
`.SchLib`: the parameter set, where each value comes from, and how the generated Altium script
|
||||
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/CHANGELOG.xlsx` — global version/parameter changelog (created on first add;
|
||||
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
|
||||
the changelog.
|
||||
- `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
|
||||
per-part `<tag>.xlsx`, so the `.SchLib` carries **every filled datasheet parameter** (not just
|
||||
the SOP minimum); skips identity/version/model-link columns, renames a couple to their SOP
|
||||
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/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
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -88,8 +88,8 @@
|
|||
"template_version": 1
|
||||
},
|
||||
"CMC": {
|
||||
"skill_version": 2,
|
||||
"template_version": 2
|
||||
"skill_version": 1,
|
||||
"template_version": 1
|
||||
},
|
||||
"CMP": {
|
||||
"skill_version": 1,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
`.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**
|
||||
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
|
||||
|
|
@ -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
|
||||
|
||||
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
|
||||
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",
|
||||
"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.
|
||||
`params.json` carries your filled values (and may include a `"remove"` list — defaults to
|
||||
`["Manufacturer_Name", "Manufacturer_Part_Number"]`, the Ultra-Librarian duplicates that get
|
||||
stripped). 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. It
|
||||
handles any parameter-set size — small sets stay in Altium's mini-stream, larger ones are written
|
||||
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.
|
||||
|
||||
Fallback (apply from inside Altium):
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------- 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
|
||||
|
||||
def read_container(path):
|
||||
|
|
@ -90,30 +110,10 @@ def _uid(name):
|
|||
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):
|
||||
s=(f"|RECORD=41|IndexInSheet={idx}|OwnerPartId=1|Justification=4|FontID=2|IsHidden=T"
|
||||
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
|
||||
|
||||
|
||||
|
|
@ -138,16 +138,34 @@ def _rec_name(block):
|
|||
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):
|
||||
"""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)
|
||||
sop_names=set(params)
|
||||
desc = params.get("Description")
|
||||
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
|
||||
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)
|
||||
added=[_param_record(20+i, nm, val) for i,(nm,val) in enumerate(params.items())]
|
||||
return b''.join(kept)+b''.join(added)+tail
|
||||
|
|
@ -231,10 +249,20 @@ def rebuild(entries, content):
|
|||
|
||||
# ----------------------------------------------------------------- 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
|
||||
fields=params_json.get("parameters", {}) or {}
|
||||
fields=dict(params_json.get("parameters", {}) or {})
|
||||
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)
|
||||
|
||||
# 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":
|
||||
t=ole.openstream(e).read().decode('latin-1','ignore')
|
||||
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:
|
||||
ok=False
|
||||
ole.close()
|
||||
|
|
@ -274,8 +301,15 @@ def main():
|
|||
ap.add_argument("--schlib", required=True)
|
||||
ap.add_argument("--params", 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()
|
||||
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__":
|
||||
|
|
|
|||
Loading…
Reference in New Issue