From 6083e4ee7833f2ff14956292856887e905445de9 Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 16 Jul 2026 08:18:50 +0000 Subject: [PATCH] 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 --- SKILL.md | 89 ++++++++-------------------------------- assets/CHANGELOG.xlsx | Bin 5664 -> 5664 bytes scripts/schlib_write.py | 65 +++++++++++++++++++++++++---- 3 files changed, 74 insertions(+), 80 deletions(-) diff --git a/SKILL.md b/SKILL.md index e31dc7b..1f68148 100644 --- a/SKILL.md +++ b/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 `.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 .SchLib --params params.json \ +python scripts/schlib_write.py --schlib .SchLib \ + --from-xlsx //.xlsx --params params.json \ --out //.SchLib --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 ` (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 --typeid \ - --file /.SchLib --file /.PcbLib --file /_datasheet.pdf \ - --out /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`). diff --git a/assets/CHANGELOG.xlsx b/assets/CHANGELOG.xlsx index 153bde74dabcd4bd8f8b05b2b29bb5a07e8d3c8b..a31adbf5ec60bb10d618532c9a2e733c2f2e3126 100644 GIT binary patch delta 1777 zcmY*aS5y;-5={&)0h0h4K#FwaC?!EDQi3Mbgt9bg3nAjtb(JbjqJVUS2%$ux6lqU1 zlm(y%tZ+`B~oSC0{?wu)6DNuo0n}c}Z0000CaK@~Y zpl#r9;HqIHUrQR1CB`8I5zE~scQ3v5sQ{f_q6^*NCT~41bR(F6>OWQyi@|cjZA;-r7gMhv^=^a6 zL)4;02P!*Db`YU#fZpjYpD_j!!p=Z6`tkyA{F;vFZAw~|tFW5oD!X8L(?V83lFy}(vc+0JFJm%r- zSmHumd8EnT845;acbK`e>Ln*1Nrpq_*3QxHr__sFr#uszqs++Sv}6te3-3aLEUzl8p}Wym=m&@1C25COt_imGZli#wdBL=8CUeA%!t#7aQ&QF+o5f1 zvGnto44B6ovN9YN`xid9fB85U#SLF9M&G;2Ahf-!n0{&WGqJI2*Akez5 zErBvkEW7Ba6#*ktr!mE)ni6|3dz7QRwJV>5lXj!<7#2d{| zT9sYFE_a!u=50J@pA1;8MuKiJ_2dGNipl@IShj1$jqsUg2JOp!|Gx*KtDpR z&qj~oa2qf)O#z1#V>lyD^B{DX`}w&8H7Dt%P139N*i~Yw;Jfjfjc4fQ2b75PIHhRZ z1*W-I6!zWrAjOf;%_BNw=fZEfr|8tAbPTZyv4;itje_~!w0VWe_D3hGbF^}-YELc} ze;8w-%56HrGX=8VlUy3g#4xQ9rnheh5a-v^zJ z9Ywx1&Vg2*qo%t}1QJC7*p!fA+(X%gSF z9a=Is8aHGx5UaZ%-5UP7w%d75B~U~sCEvJD&YN^tZm1?f;IX<6>GeI~O zF_wnj%O7QKr7Dj?YLn-6G%<*&)Q~Xa&3ycuu>&1dv;Gk}`1C>e9(kdXHaoZs!>NZ+ zyWD5&I?jo|5@Wko_OS0Ru#x2X1J=`I)SY$7@jQAOK{BOLsI5vr_eknhXQxMBeNq0w zC=vRfwv^OZLDJsUF^`9vovZKctK$dUlbc$>J(8kSUk+sZ#|c!-~Ta2DOto8u)Yit0jR`9xVVGN_7nA^FBtU#|#IZ zz3^4QYxmgy*(DP%uyA-rO$7<(u;k_T$0|21kp4Pu{2Pl@g3zt0H6c^SyL_1;KL(C91KE(IJX8#40?^i7V delta 1777 zcmY*ac|6n!6rag42E#aJcUqDhk!U1WhKO}c)P{3h{WV($dx0xBUbja+h^ZD-}m0Ds>_l&CKYimdb7~}P%1rbj5@o}WaP)iWjaET{^M~@BkLSn^z8zSuJ;GpmN_b35 zS6XWtV`HtHY|`TP-`Suv&9o3$LRvHR;Na!BwVb@Ox&>gjU)}+Z!f6t?71PcIUR9hN zE?!3T`dfOu1YF2p>~xM)aT3|^oD%$ zClQBDInPG8T4VgXBh-;k4{7h)HfKM#E~w3ty^K}?B-UW-Qs^=3jrw&>SC@~I$7WF9 z7!lg#;?WxTlqmEoDaW+##Q*u|l-Nfw@Y))BlQS$;^B4_=&O-*9CTdh0#;$lT4ds!c zfhky320wp(f16c0JbPb|R|+o!9~nA+oc6Nyg~_qL%a47lylMV%w={ciWJu;cXp`|P z2sPwN!=Gu04gO>`C495VusKn+f!~WY6qCArpw1wl#D{tssz=Vr7ac~$`M+=iKDwva zW5%qmnzn;B0_+EzEH7u9XYwJF1@xuP$?R0o+UhovZ9+*=H|bW({MPP(@n34-Q8YEr zGZk?R>*0rU3Fdv$I+jZfic_{uO}0uzQRjT`u_VhR*9;K|T9ce*xoPONINQ&frnhWu% zOz1mtprqJeb<_n)iY6Ecr|ks7^!cu`6=h&|RGNdDI^?J`Z`|EsQ=|CHPkL0l67-a) zVQ~t)Ym!A}SzYxZQwc*vy=;`vxJDPNmkj;6`)yDJ+pY8lbOH-3CR+bKDndDbBLK;N z($7)q@)ZekLxJV)(JYNB7hnT{3kgk#E6J~Na^}&UAIe}QULHOnpM!rxFFhK$+oIEo z7!4dh;254Ia@NXFC_1XeA|bwTR@RN zy(20m1Ok0P(vM3bfWY8PUHJCVgoC>2xfQw;J00gjO#cPe7f!hp65ovTcKBnN|Jp}R zIlKIf#@RlJT=C6$=JUz+N#(Ay>~mQ|2Q@DaRS3c}*C9oWwbLEmkjrVSR05&QfI>q! zlC@fM1J%??61l%Y*jyEHtvWy8IqSKf3#+H~M;CSjcD)vea84j+ltv%zC@#z&QXNgq zk!@d1-|yyh{Q5%yLQJogvYb(DDF;B)SDWwu5h1D_WsYD|ggtSpFW^4In|E9@ZP|{= zOR+&yZnY6z@S=OAL^592W}V*hfhS#Jv!tVbQpY9u3maTEqJ4{Y)$h~q?>)W`2~miN zBt9|>I@|%&vPgu9s6$~|s561q{jd_sn`_N2LZ-I~xnh<_9H&pW+a2-4%c50_?Oy9+ z1niqXkI%;SO9j=#95#_ID#>MeVd@rcOcFMc(@1al9Q(k<=HyF=1~w^=U_UqG8d;TU zKG9L9q~A!ocXUv?OaUEh;T+tv)-dO{Vt>6l-V6{A@1K_UJF(LPwO&xudDrvoP&yKItc#?~d<=8^bio&jlmnFGwMMiiJwi z0LyL*^)Zi<>RBDbdx8%)M{`U|!hs0|>FmC0m8f&5FaDcZ-xZF{c%DDJZM^Le)^l+Y z7Y-?!Dr;QSFfKwBjD{traA&fVe3`4xF|#%6I8PMFlt26GeGd-F4)eFT#jQo+kV$od zE&Gkq&gfF9v8o8a9!+m5Uc03T(*{tvAzt{Hrzfamy2&b~`Mxt(X!!6XmG!Ww`X}Ih zNkVBJF8rmKDcg@mzx**B|x+aS$ZKo9}KEfcCkGloFL|&{E483UcwB z`$(ez$QZ-8QNJT;UWuwwXd*K(zfv24K{i&n37Ht()NRn`D1^WoLB_a)3|;+&TAzi2 zku9cpBM=+_fc~c!x`*_{-Wj!p.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 `.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 (`.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 .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__":