Remove Altium 365 Part Request end task (skill ends at Gitea push); schlib_write.py: source full params + Description from the verified per-part xlsx via --from-xlsx
parent
2719f5c75b
commit
6083e4ee78
89
SKILL.md
89
SKILL.md
|
|
@ -341,15 +341,27 @@ empty until filled. The full method for the second-source search is in
|
|||
`references/schlib_parameters.md`.
|
||||
|
||||
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):
|
||||
params above (see `references/schlib_parameters.md`) — and **source it from the verified per-part
|
||||
Excel** so the symbol and the workbook can never disagree. After the sheet is verified (step 5),
|
||||
pass that `<tag>.xlsx` to the writer with `--from-xlsx`: every engineering column **and the
|
||||
Description** are read straight out of it and written into the `.SchLib`. Then layer on the
|
||||
**SOP-only** fields that aren't template columns (the `Value` shorthand, `Manufacturer Part`,
|
||||
`Operating Temperature`, `ROHS`, `Datasheet`, `Process`, `Vecmocon Part Code`, and the blank
|
||||
second-source pair) via a small `params.json`; on any name collision the `params.json` value
|
||||
wins. Pass `--typeid` too, so any template column the datasheet left silent is still present
|
||||
(blank):
|
||||
|
||||
```bash
|
||||
python scripts/schlib_write.py --schlib <in>.SchLib --params params.json \
|
||||
python scripts/schlib_write.py --schlib <in>.SchLib \
|
||||
--from-xlsx <stage>/<tag>/<tag>.xlsx --params params.json \
|
||||
--out <stage>/<tag>/<sym>.SchLib --typeid <TYPEID>
|
||||
```
|
||||
|
||||
(`--params` is optional if the Excel already carries everything you need; `--from-xlsx` is
|
||||
optional if you'd rather hand-build the whole set in `params.json` — give at least one.) The
|
||||
`Description` written onto the symbol (and into the component's ComponentDescription field) is
|
||||
the exact one from the Excel, which was built to `references/description_format.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
|
||||
|
|
@ -358,61 +370,6 @@ 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.
|
||||
|
||||
### The end-to-end workflow (this is the standard run)
|
||||
|
||||
When the central library is a managed Altium 365 Workspace, a full run goes:
|
||||
|
||||
1. **Datasheet + make** → extract parameters, classify to a typeid, fill the per-part Excel
|
||||
workbook, human-verify (the existing flow).
|
||||
2. **Operator provides the `.SchLib` + `.PcbLib`** → write the full parameter set into the
|
||||
`.SchLib` with `schlib_write.py --typeid <ID>` (template columns + SOP params, plus the
|
||||
ComponentDescription), exactly as before.
|
||||
3. **Push to Gitea** (part folder + symbol/footprint, per the normal push steps).
|
||||
4. **Prepare the Altium 365 Part Request and hand it to the local runner** — the automatic tail:
|
||||
a. Build the manifest:
|
||||
```bash
|
||||
python scripts/build_part_request_manifest.py --params <part params.json> --typeid <ID> \
|
||||
--file <inbox>/<MPN>.SchLib --file <inbox>/<MPN>.PcbLib --file <inbox>/<MPN>_datasheet.pdf \
|
||||
--out <inbox>/part_requests.json
|
||||
```
|
||||
(`build_part_request_manifest.py` maps the typeid to the Workspace Component Type — e.g.
|
||||
`CER → Capacitors` — pulls manufacturer/MPN/Description/parameters from the params, and lists
|
||||
the local attachment paths.)
|
||||
b. **Commit the `.SchLib`, `.PcbLib`, datasheet, and `part_requests.json` into the operator's
|
||||
Altium Runner inbox** on their PC via the device bridge (the `inbox` folder in
|
||||
`runner_config.json`, e.g. `C:/Altium Runner/inbox`). The manifest's `files` paths must be
|
||||
those on-disk inbox paths.
|
||||
5. **The local runner finishes it, hands-off.** `scripts/local_runner.py` (installed once on the
|
||||
operator's machine as a Task Scheduler job at logon) watches the inbox, ensures Chrome is up
|
||||
with the debug port on the signed-in profile, and runs `altium365_part_request.py`, which
|
||||
**fills the Part Request completely — fields, parameters, and all three attachments — and
|
||||
leaves it on screen without saving** (review mode). The operator just **reviews and clicks
|
||||
Save**. Nothing is submitted without their click.
|
||||
|
||||
So the operator's only actions per component are giving the datasheet+make, then the
|
||||
`.SchLib`+`.PcbLib`, then reviewing the filled Part Request and clicking Save. Everything between
|
||||
is automatic, and the submission step costs zero Claude tokens. Standing requirements: the
|
||||
operator's machine is on, the runner is installed, and its Chrome profile has been signed into the
|
||||
Workspace once (the runner relaunches Chrome with the debug port and reuses the session).
|
||||
|
||||
## Per-typeid versioning
|
||||
|
||||
Versioning is **per typeid**, not global. Each typeid carries its own `template_version` and
|
||||
|
|
@ -572,20 +529,6 @@ 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** (no token/admin): prerequisites, per-component field mapping,
|
||||
the `part_requests.json` manifest, the captured form selectors, and both routes (live
|
||||
Claude-in-Chrome, or the standalone Selenium script).
|
||||
- `scripts/altium365_part_request.py` — standalone **Selenium** submitter: attaches to the
|
||||
operator's signed-in Chrome and loops the `part_requests.json` manifest, filling and saving each
|
||||
Part Request. Token-free per component; the token-economical end task for a whole library.
|
||||
- `scripts/build_part_request_manifest.py` — build/append `part_requests.json` from a component's
|
||||
schlib `params.json` + local file paths; maps typeid → Altium Component Type. Run at the end of
|
||||
a run, before committing the manifest+files to the operator's inbox.
|
||||
- `scripts/local_runner.py` (+ `runner_config.example.json`) — the operator installs this **once**
|
||||
on their machine; it watches the inbox for manifests the skill drops there, ensures Chrome's
|
||||
debug session, and runs the Selenium submitter in **review mode** (fills everything, leaves the
|
||||
form for the operator to review + Save) — the automatic, hands-off tail of every run.
|
||||
- `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`).
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -18,8 +18,18 @@ What it does to the target component's Data stream:
|
|||
- leaves pins, graphics, the Comment and all other records exactly as they were.
|
||||
|
||||
Usage:
|
||||
# source parameters + Description from the verified per-part workbook (preferred):
|
||||
python schlib_write.py --schlib IN.SchLib --from-xlsx <tag>.xlsx \
|
||||
[--params sop.json] --typeid ELE --out OUT.SchLib
|
||||
# or from a params.json alone:
|
||||
python schlib_write.py --schlib IN.SchLib --params params.json --out OUT.SchLib
|
||||
|
||||
--from-xlsx reads the filled per-part `<tag>.xlsx` (the same workbook the engineer verified) and
|
||||
writes every engineering column + the Description into the symbol, so the .SchLib and the sheet
|
||||
never diverge. --params then layers on the SOP-only fields that aren't template columns
|
||||
(Value shorthand, Process, ROHS, Datasheet, Manufacturer Part, Vecmocon Part Code, ...); on a
|
||||
name collision the params.json value wins. Give either or both (at least one).
|
||||
|
||||
params.json (same shape altium_params.py uses):
|
||||
{"component":"JMK105BJ105KV-F", # LibRef / component-storage name; omit -> all comps
|
||||
"parameters":{"Value":"1u","Manufacturer":"Taiyo Yuden", ...},
|
||||
|
|
@ -62,6 +72,28 @@ def template_param_names(template_path, typeid):
|
|||
if ws.cell(1, c).value and ws.cell(1, c).value not in NON_PARAM_COLS]
|
||||
|
||||
|
||||
def params_from_xlsx(xlsx_path, sheet=None):
|
||||
"""Read the filled parameter values out of a per-part workbook (`<tag>.xlsx`) — row 1 is the
|
||||
headers, row 2 is the single data row this skill writes. Returns {header: value} for every
|
||||
engineering column (i.e. NOT the bookkeeping/design columns in NON_PARAM_COLS), INCLUDING the
|
||||
Description column. This is what lets the .SchLib carry exactly the parameters + Description
|
||||
the engineer already verified in Excel — the two never diverge. Blank cells stay blank.
|
||||
|
||||
By default it reads the first sheet (the typeid parameter sheet). A `Version History` second
|
||||
sheet, if present, is skipped."""
|
||||
import openpyxl
|
||||
wb = openpyxl.load_workbook(xlsx_path, read_only=True, data_only=True)
|
||||
ws = wb[sheet] if sheet else wb[wb.sheetnames[0]]
|
||||
headers = [ws.cell(1, c).value for c in range(1, ws.max_column + 1)]
|
||||
values = [ws.cell(2, c).value for c in range(1, ws.max_column + 1)]
|
||||
out = {}
|
||||
for h, v in zip(headers, values):
|
||||
if not h or h in NON_PARAM_COLS:
|
||||
continue
|
||||
out[str(h)] = "" if v is None else str(v)
|
||||
return out
|
||||
|
||||
|
||||
# ----------------------------------------------------------------- read the container
|
||||
|
||||
def read_container(path):
|
||||
|
|
@ -249,11 +281,21 @@ def rebuild(entries, content):
|
|||
|
||||
# ----------------------------------------------------------------- driver
|
||||
|
||||
def write_params(schlib, params_json, out, typeid=None, template=None):
|
||||
def write_params(schlib, params_json, out, typeid=None, template=None, from_xlsx=None):
|
||||
params_json = params_json or {}
|
||||
component=params_json.get("component") or None
|
||||
fields=dict(params_json.get("parameters", {}) or {})
|
||||
remove=params_json.get("remove", DEFAULT_REMOVE)
|
||||
|
||||
# Base the parameter set on the verified per-part Excel when given (--from-xlsx): every
|
||||
# engineering column and the Description come straight from the workbook the engineer already
|
||||
# verified, so the symbol and the sheet can never disagree. Explicit params.json entries
|
||||
# (the SOP-only fields like Value/Process/ROHS/Datasheet/Vecmocon Part Code that aren't
|
||||
# template columns) are layered on top and win on any name collision.
|
||||
fields = {}
|
||||
if from_xlsx:
|
||||
fields.update(params_from_xlsx(from_xlsx))
|
||||
fields.update(params_json.get("parameters", {}) or {})
|
||||
|
||||
# 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,
|
||||
|
|
@ -299,17 +341,26 @@ def write_params(schlib, params_json, out, typeid=None, template=None):
|
|||
def main():
|
||||
ap=argparse.ArgumentParser()
|
||||
ap.add_argument("--schlib", required=True)
|
||||
ap.add_argument("--params", required=True)
|
||||
ap.add_argument("--params", help="params.json with SOP fields (Value/Process/ROHS/Datasheet/"
|
||||
"Vecmocon Part Code/...) + optional 'component'/'remove'. "
|
||||
"Optional if --from-xlsx supplies the parameters.")
|
||||
ap.add_argument("--from-xlsx", dest="from_xlsx",
|
||||
help="the verified per-part <tag>.xlsx: every engineering column + the "
|
||||
"Description are written into the .SchLib straight from it, so the "
|
||||
"symbol matches the sheet. Layer SOP-only fields on with --params.")
|
||||
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("--typeid", help="component typeid; with --template, guarantees that typeid's "
|
||||
"full template parameter set is present (blank where absent)")
|
||||
ap.add_argument("--template", help="path to template.xlsx (defaults to the skill's)")
|
||||
a=ap.parse_args()
|
||||
if not a.params and not a.from_xlsx:
|
||||
ap.error("give --params and/or --from-xlsx (at least one source of parameters)")
|
||||
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)
|
||||
params_json = json.load(open(a.params, encoding="utf-8")) if a.params else {}
|
||||
write_params(a.schlib, params_json, a.out,
|
||||
typeid=a.typeid, template=template, from_xlsx=a.from_xlsx)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
Loading…
Reference in New Issue