app
adom-desktop-altium-bridge
Public Unreviewedby barrett-land
Adom Desktop bridge for Altium Designer — build and edit file-based symbol/footprint/Integrated libraries with LCSC/JLCPCB part numbers attached, and generate batch outputs (STEP, PDF, Gerber, BOM, CPL) from an OutJob, by driving Altium via DelphiScript.
← Commit history
Publish 0.0.41
15 files changed
+104−38
README.md+1−1SKILL.md+39−11adom-desktop-altium-bridge/bridge-src/__pycache__/addin_client.cpython-312.pycadom-desktop-altium-bridge/bridge-src/__pycache__/altium_detect.cpython-312.pycadom-desktop-altium-bridge/bridge-src/__pycache__/check_wiring.cpython-312.pycadom-desktop-altium-bridge/bridge-src/__pycache__/server.cpython-312.pycadom-desktop-altium-bridge/bridge-src/__pycache__/test_addin_client.cpython-312.pycaltium-bridge-manifest.json+3−3altium-bridge-v0.0.41.zipbridge-src/BRIDGE_VERSION+1−1bridge-src/__pycache__/server.cpython-312.pycbridge-src/bridge.json+1−1bridge-src/server.py+54−17package.json+1−1page.json+4−3README.md+1−1@@ -11,7 +11,7 @@ verbs. ```bash adom-wiki pkg install barrett-land/adom-desktop-altium-bridge # the skill, in the container-adom-desktop bridge_install '{"manifestUrl":"https://wiki.adom.inc/download/barrett-land/adom-desktop-altium-bridge/0.0.40/altium-bridge-manifest.json"}'+adom-desktop bridge_install '{"manifestUrl":"https://wiki.adom.inc/download/barrett-land/adom-desktop-altium-bridge/0.0.41/altium-bridge-manifest.json"}' adom-desktop --ai-thread "<your thread>" altium_ping '{}' ```
SKILL.md+39−11@@ -12,7 +12,7 @@ forwards work into the running GUI as DelphiScript via `X2.EXE`. ## Install ```bash-adom-desktop bridge_install '{"manifestUrl":"https://wiki.adom.inc/download/barrett-land/adom-desktop-altium-bridge/0.0.40/altium-bridge-manifest.json"}'+adom-desktop bridge_install '{"manifestUrl":"https://wiki.adom.inc/download/barrett-land/adom-desktop-altium-bridge/0.0.41/altium-bridge-manifest.json"}' adom-desktop --ai-thread "<your thread>" altium_ping '{}' # -> {"altiumInstalled": true, "version": "..."} ``` @@ -82,18 +82,46 @@ adom-desktop --ai-thread "<thread>" altium_run_outjob '{ - Always pass `project`. The script opens **and compiles** it first, which is what populates an OutJob's `[Project Physical Documents]` target. Without it the OutJob has nothing to act on.-- Pass `expect` to get an authoritative `out_exists` in the reply instead of inferring success.+- Pass `expect`. With it, the verb checks the filesystem and reports `ok: false, triggered: false`+ when the run produced nothing, instead of a hollow success. - An OutJob targeting `[Project Physical Documents]` is **generic and reusable** across boards — add it to the project, compile, run.-- ⚠️ **Known limitation:** `GenerateReport` is observed to no-op in configurations where the GUI's- *Generate content* button on the same OutJob works. `ran: true` with `out_exists: false` means- the trigger did not fire — it does **not** mean the OutJob is misconfigured. Ask the user to click- *Generate content* once rather than rebuilding a working OutJob.--`altium_export_step` opens a `.PcbDoc` and reports that the board resolved. With `run: true` it also-fires the process named by `proc` (default `Pcb:ExportSTEP`, overridable so candidates can be probed-without redeploying the script). If that process opens a modal, the call times out with the export-possibly still pending — **check the output path on disk before concluding it failed.**++### What generates headlessly, and what doesn't++| Output type | Headless via `altium_run_outjob`? |+|---|---|+| **Gerber** (`.GTL` copper, `.GTO` silk, `.GTS` mask, `.GM1` outline) | ✅ yes |+| **ExportSTEP** | ❌ no — `GenerateReport` no-ops; a human must click *Generate content* once |++The STEP exception is not a misconfiguration. It was verified with+`ObjectKind=OutputBatch/OutputMedium/Document`, with and without `OutputMedium`, and with the+OutJob already a member of a compiled project. Ask the user to click the button rather than+rebuilding an OutJob that is already correct.++### ⚠️ There is no headless STEP-export process++`altium_export_step` **opens a board and reports it resolved — it does not export.** Every+`Command='...'` in AD26's resource files was enumerated:++- `Pcb:ExportSTEP` **does not exist.** It was this verb's old default, and because Altium silently+ ignores unknown process ids, the call returned `ran: true` and wrote no file.+- The real one, `PCB:Export`, is index-based (`Group=Model`) and opens a file dialog.++`run: true` now demands an explicit `proc` (a probe hatch only) and fails loudly when no file+appears. **To actually get a STEP, use an ExportSTEP OutJob plus one human click.**++### The STEP has no copper and no silkscreen++Altium's STEP is the board substrate plus component 3D bodies — nothing else. A render made from+it alone is a bare green board. Rebuild both from Gerbers:++- **silkscreen** = `.GTO`+- **exposed pads** = `.GTL` ∩ `.GTS`. `.GTS` is `TF.FilePolarity,Negative`, so its features are the+ mask *openings*; all of `.GTL` is ~76% of the board because most copper is a covered pour.++Composite them into one RGBA overlay and pass it to `step2glb --silk-top` (which only takes effect+with `--molecule`). The full recipe lives in the **altium-export-molecule** skill. ## Developing the bridge
adom-desktop-altium-bridge/bridge-src/__pycache__/addin_client.cpython-312.pycadded⋯ 1 unchanged line ⋯
adom-desktop-altium-bridge/bridge-src/__pycache__/altium_detect.cpython-312.pycadded⋯ 1 unchanged line ⋯
adom-desktop-altium-bridge/bridge-src/__pycache__/check_wiring.cpython-312.pycadded⋯ 1 unchanged line ⋯
adom-desktop-altium-bridge/bridge-src/__pycache__/server.cpython-312.pycadded⋯ 1 unchanged line ⋯
adom-desktop-altium-bridge/bridge-src/__pycache__/test_addin_client.cpython-312.pycadded⋯ 1 unchanged line ⋯
altium-bridge-manifest.json+3−3@@ -1,3 +1,3 @@-{ "manifest_version": 1, "name": "altium", "version": "0.0.40",- "url": "https://wiki.adom.inc/download/barrett-land/adom-desktop-altium-bridge/0.0.40/altium-bridge-v0.0.40.zip",- "sha256": "49a6ac3d9ca77f5be2c43a1c1f257a427fd99a8296f93391b8faf105f6042ec1", "size": 26085, "released_at": "2026-08-05T20:55:37Z" }+{ "manifest_version": 1, "name": "altium", "version": "0.0.41",+ "url": "https://wiki.adom.inc/download/barrett-land/adom-desktop-altium-bridge/0.0.41/altium-bridge-v0.0.41.zip",+ "sha256": "ce614444afefcdffa56f4d03e57ff4fdd4bd13d2f8b59859477f2b192db9c92f", "size": 26776, "released_at": "2026-08-05T22:16:21Z" }
altium-bridge-v0.0.41.zipadded⋯ 1 unchanged line ⋯
bridge-src/BRIDGE_VERSION+1−1@@ -1 +1 @@-0.0.40+0.0.41
bridge-src/__pycache__/server.cpython-312.pyc⋯ 1 unchanged line ⋯
bridge-src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "altium", "displayName": "Altium Bridge",- "version": "0.0.40",+ "version": "0.0.41", "description": "Drives Altium Designer from a container: build, clone and edit file-based symbol/footprint/Integrated libraries with LCSC/JLCPCB part numbers attached, and generate batch outputs (STEP, PDF, Gerber, BOM, CPL) from an OutJob. Pairs with the container-side altium-jlc orchestrator (adom-jlcpcb + chip-fetcher).", "homepage": "https://wiki.adom.inc/adom/adom-desktop-altium-bridge", "author": "Adom Inc.",
bridge-src/server.py+54−17@@ -277,29 +277,49 @@ def handle_script_state(args: dict) -> dict: def handle_export_step(args: dict) -> dict:- """Export a .PcbDoc's 3D board assembly to STEP.+ """Open a .PcbDoc and report that its board resolved. Does NOT export by default. - Two modes. Without run=true it only OPENS the board and reports that it resolved- (the safe probe). With run=true it also fires the export process named by 'proc'- (default Pcb:ExportSTEP) writing to 'out'.+ There is NO headless board->STEP process in Altium. Every `Command='...'` in+ AD26's resource files was enumerated: the old default here, `Pcb:ExportSTEP`,+ does not exist, and Altium silently ignores unknown process ids — so the call+ reported ran=true and wrote nothing. The real process, `PCB:Export`, is+ index-based (`Group=Model`) and opens a file dialog, so it cannot be driven+ headlessly either. - Altium exposes no documented headless board->STEP process, so 'proc' is- overridable to probe candidates without redeploying the script. If the process- opens a modal dialog the script blocks and never writes a response — the call- times out with the export possibly still pending. Check 'out' on disk before- assuming failure.+ Use `altium_run_outjob` with an OutJob carrying an ExportSTEP output instead.+ `run` is kept only as a probe hatch for trying a candidate process id, and now+ demands an explicit `proc` rather than guessing one that cannot work. """ if not args.get("pcbdoc"): return {"ok": False, "error": "missing 'pcbdoc' (path to the .PcbDoc on the desktop)"}- if args.get("run") and not args.get("out"):- return {"ok": False, "error": "run=true also needs 'out' (destination .step path)"}++ if args.get("run"):+ if not args.get("proc"):+ return {+ "ok": False,+ "error": "run=true needs an explicit 'proc'; there is no working default.",+ "_hint": "Altium has no headless STEP-export process. To actually export, use "+ "altium_run_outjob with an OutJob containing an ExportSTEP output. Pass "+ "'proc' here only to probe a candidate process id.",+ }+ if not args.get("out"):+ return {"ok": False, "error": "run=true also needs 'out' (destination .step path)"}+ params = { "pcbdoc": args.get("pcbdoc"), "run": "true" if args.get("run") else None, "out": args.get("out"), "proc": args.get("proc"), }- return addin_call("export_step", params, timeout=args.get("timeout", 240))+ res = addin_call("export_step", params, timeout=args.get("timeout", 240))++ # An unknown process id still comes back ran=true. Only the file on disk is proof.+ if isinstance(res, dict) and res.get("ran") and not res.get("out_exists"):+ res["ok"] = False+ res["error"] = (f"process {args.get('proc')!r} produced no file at {args.get('out')!r} — "+ "it most likely does not exist (Altium ignores unknown process ids).")+ res["_hint"] = "Use altium_run_outjob with an ExportSTEP OutJob; that path is proven."+ return res def handle_run_outjob(args: dict) -> dict:@@ -311,10 +331,15 @@ def handle_run_outjob(args: dict) -> dict: Pass 'expect' (a path the run should produce) to get an authoritative out_exists in the reply instead of guessing from the process return. - KNOWN LIMITATION: GenerateReport is observed to no-op in some configurations- where the GUI's 'Generate content' button on the same OutJob works. Treat- out_exists=false with ran=true as "the trigger did not fire", not "the OutJob is- misconfigured", and verify by hand before rebuilding the OutJob.+ WHAT WORKS, MEASURED: Gerber outputs generate fine headlessly through this verb.+ `ExportSTEP` outputs are the exception — GenerateReport no-ops on them while the+ GUI's 'Generate content' button on the SAME OutJob works. Verified with+ ObjectKind=OutputBatch/OutputMedium/Document, with and without OutputMedium, and+ with the OutJob already a member of a compiled project, so none of those are the+ cause. For STEP, ask the user to click Generate content once.++ Pass `expect` and this verb will tell you the trigger failed instead of reporting+ a hollow success. """ if not args.get("outjob"): return {"ok": False, "error": "missing 'outjob' (path to the .OutJob on the desktop)"}@@ -331,7 +356,19 @@ def handle_run_outjob(args: dict) -> dict: for key in ("reload", "disable_dialog"): if key in args: params[key] = "false" if args[key] in (False, "false") else "true"- return addin_call("run_outjob", params, timeout=args.get("timeout", 300))+ res = addin_call("run_outjob", params, timeout=args.get("timeout", 300))++ # GenerateReport reports success even when it silently did nothing, which is how a+ # no-op reads as a working export. If the caller told us what to expect, believe+ # the filesystem over the process.+ if isinstance(res, dict) and args.get("expect") and res.get("ran") and not res.get("out_exists"):+ res["ok"] = False+ res["triggered"] = False+ res["error"] = f"OutJob ran but produced nothing at {args['expect']!r}."+ res["_hint"] = ("GenerateReport no-ops on ExportSTEP outputs (Gerber outputs are fine). "+ "Ask the user to open the OutJob and click 'Generate content' once; do not "+ "rebuild the OutJob, it is most likely correct.")+ return res COMMAND_HANDLERS = {
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-desktop-altium-bridge",- "version": "0.0.40",+ "version": "0.0.41", "type": "app", "description": "Adom Desktop bridge for Altium Designer \u2014 build and edit file-based symbol/footprint/Integrated libraries with LCSC/JLCPCB part numbers attached, and generate batch outputs (STEP, PDF, Gerber, BOM, CPL) from an OutJob, by driving Altium via DelphiScript.", "tags": [
page.json+4−3@@ -1,8 +1,8 @@ { "slug": "adom-desktop-altium-bridge",- "version": "0.0.40",+ "version": "0.0.41", "type": "app",- "description": "Adom Desktop bridge for Altium Designer \u2014 build and edit file-based symbol/footprint/Integrated libraries with LCSC/JLCPCB part numbers attached, and generate batch outputs (STEP, PDF, Gerber, BOM, CPL) from an OutJob, by driving Altium via DelphiScript.",+ "description": "Adom Desktop bridge for Altium Designer — build and edit file-based symbol/footprint/Integrated libraries with LCSC/JLCPCB part numbers attached, and generate batch outputs (STEP, PDF, Gerber, BOM, CPL) from an OutJob, by driving Altium via DelphiScript.", "tags": [ "app", "adom-desktop",@@ -18,6 +18,7 @@ }, "needs_sudo": false, "hero": {+ "type": "image", "path": "docs/hero.png" }, "author": {@@ -48,4 +49,4 @@ "prompt": "Is the Altium bridge connected and what version is it?" } ]-}+}