app
Altium - the Altium Bridge
Public Made by Adomby adom
Adom Bridge for Altium Designer: a resident DelphiScript agent gives the AI eval, the 1,100-command process launcher, all 8 EDA surfaces with screenshots, compile/ERC, DRC, OutJob outputs, native SchLib/PcbLib authoring with STEP 3D bodies, and dialog handling by PID.
← Commit history
v0.1.11: Phase 1 introspection — 11 read-only board/schematic verbs; reaper via ab verbs; last Python release before the Rust bridge
14 files changed
+833−27
CHANGELOG.md+6README.md+4−1SKILL.md+18adom-bridge-altium-manifest.json+4−4adom-bridge-altium-v0.1.11.zipbridge.json+13−2package.json+1−1skills/altium-boards/SKILL.md+10skills/altium-bridge-dev/SKILL.md+37−1src/BRIDGE_VERSION+1−1src/agent/AdomAgent.pas.tmpl+591−1src/bridge.json+13−2src/handlers/agent.py+49−13src/server.py+86−1CHANGELOG.md+6@@ -1,5 +1,11 @@ # Changelog +## 0.1.11 — 2026-08-31+- Phase 1 of the parity plan: 11 read-only board/schematic introspection verbs — list_designators, component_data, component_pins, list_nets, net_connections, layers, copper_layers, schematic_data (pins + part IDs), read_rules, set_layer_visibility, footprint_primitives. All verified live on Bluetooth Sentinel (16–350 ms), zero dialogs.+- Busy-notice reaper rewritten to go through ab's desktop verbs (find_window → find_control → ui_click) with a 4 s watch, after the X2-pid scan missed the notice twice on a live box.+- Parked: create_net_class (IPCB_ObjectClass.SuperClass/.Name are undeclared in AD26 scripting) — function kept, not dispatched.+- Last Python release of the bridge process: the next major moves the bridge to a Rust exe that calls ab's verbs and shrinks the DelphiScript agent to generic dump/apply primitives (ab#987 requests the binary runtime kind).+ ## 0.1.10 — 2026-08-31 - #3 (Colby) root-cause FIX: the agent registers the PCB/SCH scripting APIs at startup (`ServersUp`: `Client.StartServer('PCB')`/`('SCH')` before the first `PCBServer` reference) — on a fresh session that never opened a PCB document the API unit is unregistered and the old template halted with an "Undeclared identifier: PostProcess" modal on the user's screen. - #3 follow-up: `altium_agent_recover` now closes the scratch `AdomAgent_<digest>.PrjScr` out of the Projects panel after freeing the engine (only there — after a successful start the resident loop runs from it).
README.md+4−1@@ -106,7 +106,10 @@ A halted engine (DelphiScript runtime errors are modal) recovers with `altium_ag ## Verb reference -All 81 verbs (prefix `altium_`). Call `altium_describe` for the live catalog.+**Board & schematic introspection (0.1.11):** `list_designators`, `component_data`, `component_pins`, `list_nets`, `net_connections`, `layers`, `copper_layers`, `read_rules`, `set_layer_visibility`, `footprint_primitives`, `schematic_data` — read the live design as data (pads→nets, rules, layers, sheet components with pins), all in tens of milliseconds, no dialogs.+++All 94 verbs (prefix `altium_`). Call `altium_describe` for the live catalog. **Readiness & lifecycle** — `readiness`, `status`, `launch`, `close`, `diagnostics`, `progress`, `agent_start/_stop/_status/_recover`, `license`, `signin_status`, `connect_workspace`, `uninstall`
SKILL.md+18@@ -102,3 +102,21 @@ adom-bridge --ai-thread "board review" altium_export_gerber '{"outjobPath":"C:/d ## How this bridge is packaged Wiki page `adom/altium-bridge`: the Release zip + `adom-bridge-altium-manifest.json` (what Adom Bridge installs with `bridge_install`), and this skills pkg (`adom-wiki pkg install adom/altium-bridge` — skills only; it does not install the runtime). Plan of record and every measured engine fact: the `altium-bridge-plan` skill.++## Board & schematic introspection (0.1.11)++Read-only, need a .PcbDoc (or .SchDoc for `schematic_data`) as the CURRENT document — the agent payload comes back under `data`.++| Verb | Returns |+|---|---|+| `altium_list_designators` | Every component designator on the current board. |+| `altium_component_data` | Per-component data: pattern, comment, side, x/y/rotation, bbox, pad count, pad→net map ({designator} for one, else all). |+| `altium_component_pins` | Every pad of one component: name, net, position, size, hole. |+| `altium_list_nets` | Every net with its pad count. |+| `altium_net_connections` | Every pad with net + owner (DES.PIN, x/y) and per-net span; {net} filters. |+| `altium_layers` | The board layer set with names, kinds, visibility. |+| `altium_copper_layers` | All 48 copper layers (top, mid 1-30, planes 1-16, bottom). |+| `altium_schematic_data` | Components on the current sheet with pins (number/name/electrical) and part IDs. |+| `altium_read_rules` | Every design rule: name, kind, enabled, priority, scopes. |+| `altium_set_layer_visibility` | Show/hide a layer {layerId, displayed}. |+| `altium_footprint_primitives` | Geometry dump of a placed component: tracks, arcs, pads, texts. |
adom-bridge-altium-manifest.json+4−4@@ -1,9 +1,9 @@ { "name": "altium",- "version": "0.1.10",- "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.10/adom-bridge-altium-v0.1.10.zip",- "sha256": "8b6e93dc5abaa53c024d86152329a151581170f97d28b773612e3c340ce4eafc",- "size": 97615,+ "version": "0.1.11",+ "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.11/adom-bridge-altium-v0.1.11.zip",+ "sha256": "3f5062567ccffa208025b7cc4bdb51b7a06bce047c5d0c8e1cf6f9019478ff8d",+ "size": 102890, "verbPrefixes": [ "altium_" ],
adom-bridge-altium-v0.1.11.zipadded⋯ 1 unchanged line ⋯
bridge.json+13−2@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "altium", "displayName": "Altium Designer",- "version": "0.1.8",+ "version": "0.1.11", "description": "Drive Altium Designer from Adom: a resident DelphiScript agent (eval + the 1,100-command process launcher), the X2 -R command line, UIA over the VCL shell, dialogs by PID, screenshots of every Altium window, and Altium 365.", "homepage": "https://wiki.adom.inc/adom/altium-bridge", "docs": "https://wiki.adom.inc/adom/altium-bridge",@@ -103,7 +103,18 @@ "altium_set_visibility", "altium_xcopy_footprint", "altium_import_part",- "altium_install_library_bundle"+ "altium_install_library_bundle",+ "altium_list_designators",+ "altium_component_data",+ "altium_component_pins",+ "altium_list_nets",+ "altium_layers",+ "altium_net_connections",+ "altium_schematic_data",+ "altium_copper_layers",+ "altium_read_rules",+ "altium_set_layer_visibility",+ "altium_footprint_primitives" ], "statusVerb": "altium_status", "uninstall": "altium_uninstall",
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "altium-bridge",- "version": "0.1.10",+ "version": "0.1.11", "type": "app", "description": "Skills for your container so your AI knows how to drive the Altium Designer bridge (altium_* verbs). The bridge runtime itself is the release zip; Adom Bridge loads that.", "tags": [
skills/altium-boards/SKILL.md+10@@ -59,3 +59,13 @@ their nets. Trust the report, not a return code. text bloats the bbox. - **Frames under a locked Windows session are BLACK** (DirectX): check `desktop_get_foreground` ≠ 0 before trusting a board frame; GDI editors still capture.++## Reading the board as data (0.1.11)++`list_designators`, `component_data {designator?}`, `component_pins {designator}`, `list_nets`,+`net_connections {net?}` (pads as DES.PIN with coordinates + per-net span), `layers` /+`copper_layers`, `read_rules`, `footprint_primitives {designator}`, `set_layer_visibility+{layerId, displayed}`; `schematic_data {designator?, pins?}` on the current sheet. They read the+CURRENT document: opening a .SchDoc makes board verbs answer "no current PCB board" until a+.PcbDoc is opened again — order your calls. Payload is under `data`. Multi-part symbols list+once per part (`partId`).
skills/altium-bridge-dev/SKILL.md+37−1@@ -32,6 +32,22 @@ Everything you need to change the bridge without re-learning what this session p running old code); to load NEW PYTHON (server.py/handlers), `bridge_kill` (reloads the process). Both often needed together. +## Deploying a big template (measured 2026-08-31)++`write_file` passes the content as a CLI argument; once `AdomAgent.pas.tmpl` passed ~130 KB it+failed with "Argument list too long" and NOTHING deployed (the reload then ran old code —+"Unknown command" for the new verbs, zero dialogs, easy to misread as a compile pass). Deploy+big files with `send_files {filePaths:[...]}` (streams; ignores destDir and lands in+`C:\Users\john\Downloads`) then `Move-Item` into `bridges-cache\altium\{agent\,}`.++## "Current document" semantics++`PCBServer.GetCurrentPCBBoard` / `SchServer.GetCurrentSchDocument` follow the ACTIVE document.+Opening a SchDoc makes board verbs answer "no current PCB board" until a PcbDoc is opened+again. Read verbs return that error with a `_hint`; order your calls, or open the right doc first.+Schematic iteration needs the sheet SHOWN (activated) — a background `open_document` can leave+`SchIterator` empty (measured: count 0 until `show_schematic`).+ ## Dialog pre-emption (John: a dismissed dialog is a defect) The DelphiScript debugger breaks on ANY raised exception even inside Try/Except — a raise IS@@ -47,7 +63,7 @@ Warning. When you add a mutating command, wrap file reads and never leave a tran `IPCB_BoardOutline.Clear`, `eProcessFreePrimitives`, `IPCB_Board.GroupIterator_Create` (use `BoardIterator_Create`), `IPCB_Component.Name.Text :=` (use `NameOn`/`Inst.Name.MoveToXY`), `GroupIterator` without `AddFilter_LayerSet(AllLayers)` (mech-layer children invisible),-`RunProcessWithParameters` (use ResetParameters+AddStringParameter+RunProcess), a nested `{}`+`RunProcessWithParameters` (use ResetParameters+AddStringParameter+RunProcess), `IPCB_ObjectClass.SuperClass :=` (not declared in AD26 scripting — a new class from `PCBClassFactory` is already non-super; just omit it), a nested `{}` inside a brace comment (breaks compilation). An unknown identifier is a RUNTIME error that halts the debugger; `altium_agent_recover` clears it. @@ -57,6 +73,26 @@ Result strings build JSON with `'{' + ... + '}'`; that's fine in a STRING LITERA BRACE COMMENTS that cannot contain a nested `{`. Lint new commands: `re.sub(r"'[^']*'","",code)` then assert no `\{[^}]*\{`. +## Two sessions on one engine = a modal storm (measured 2026-08-31)++The resident loop is ONE script engine. When a second session (another Claude thread, a+delegated agent) also drives `altium_*` verbs, a halt raised by one session's command surfaces+as the OTHER session's error (it saw "Undeclared identifier: SuperClass/Name" on a read-only+verb that was innocent), both sessions then run `agent_recover`, and every recover's `-R`+helper puts the single-instance busy notice on the user's screen. Rule: exactly one driver+per Altium instance. Coordinate ownership (cross-session message) before any second session+touches the engine, and never delegate live-engine work without that handshake.++## Recover costs the user a modal — measured 2026-08-31 on John's screen++Each `agent_recover` fires `X2 -REditScript:Stop`; against a halted (busy) instance Windows raises the+single-instance notice ("There is another instance of running, but it is busy") on the USER's+screen, and 0.1.9's `_reap_busy_notice` did NOT catch it (twice). Until the reaper is proven:+never iterate a suspected-halting command live on the user's box — halt → recover → retry is a+modal per cycle. Eval-probe identifiers in a throwaway session, or ship the verb unwired.+`create_net_class` is parked unwired for exactly this reason (`SuperClass` halts; a second+identifier halts after it).+ ## When the bridge "vanishes" from ab `bridge_list` without `altium` + `Unknown verb altium_*` = ab could not parse the cached `bridge.json`
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-0.1.10+0.1.11
src/agent/AdomAgent.pas.tmpl+591−1@@ -2910,6 +2910,585 @@ Begin End; ++{ ---- Phase 1: board introspection, read-only (altium-bridge parity plan) ---- }+{ Every pad of a component as name=net pairs, all pads, capped. }+Function AllPadNets(Comp : IPCB_Component) : String;+Var+ GIt : IPCB_GroupIterator;+ Pad : IPCB_Pad;+ S, N : String;+ K : Integer;+Begin+ S := ''; K := 0;+ GIt := Comp.GroupIterator_Create;+ GIt.AddFilter_ObjectSet(MkSet(ePadObject));+ GIt.AddFilter_LayerSet(AllLayers);+ Pad := GIt.FirstPCBObject;+ While (Pad <> Nil) And (K < 400) Do+ Begin+ N := '';+ If Pad.Net <> Nil Then N := Pad.Net.Name;+ If S <> '' Then S := S + ';';+ S := S + Pad.Name + '=' + N;+ K := K + 1;+ Pad := GIt.NextPCBObject;+ End;+ Comp.GroupIterator_Destroy(GIt);+ Result := S;+End;++Function FindCompByDes(Board : IPCB_Board; Des : String) : IPCB_Component;+Var+ BIt : IPCB_BoardIterator;+ Comp : IPCB_Component;+Begin+ Result := Nil;+ BIt := Board.BoardIterator_Create;+ BIt.AddFilter_ObjectSet(MkSet(eComponentObject));+ BIt.AddFilter_LayerSet(AllLayers);+ BIt.AddFilter_Method(eProcessAll);+ Comp := BIt.FirstPCBObject;+ While (Comp <> Nil) And (Result = Nil) Do+ Begin+ If UpperCase(Comp.Name.Text) = UpperCase(Des) Then Result := Comp;+ Comp := BIt.NextPCBObject;+ End;+ Board.BoardIterator_Destroy(BIt);+End;++Function CompJson(Comp : IPCB_Component) : String;+Var+ R : TCoordRect;+Begin+ R := Comp.BoundingRectangle;+ Result := '{' + JStr('designator', Comp.Name.Text) + ',' ++ JStr('pattern', Comp.Pattern) + ',' ++ JStr('comment', Comp.Comment.Text) + ',' ++ JRaw('layerId', IntToStr(Comp.Layer)) + ',' ++ JRaw('bottom', JBool(Comp.Layer = eBottomLayer)) + ',' ++ JRaw('xMil', IntToStr(Round(CoordToMils(Comp.x)))) + ',' ++ JRaw('yMil', IntToStr(Round(CoordToMils(Comp.y)))) + ',' ++ JRaw('rotation', IntToStr(Round(Comp.Rotation))) + ',' ++ JRaw('wMil', IntToStr(Round(CoordToMils(R.Right - R.Left)))) + ',' ++ JRaw('hMil', IntToStr(Round(CoordToMils(R.Top - R.Bottom)))) + ',' ++ JRaw('pads', IntToStr(GroupCount(Comp, ePadObject))) + ',' ++ JStr('nets', AllPadNets(Comp)) + '}';+End;++{ list_designators: every component designator on the current board }+Function CmdListDesignators(Lines : TStringList) : String;+Var+ Board : IPCB_Board;+ BIt : IPCB_BoardIterator;+ Comp : IPCB_Component;+ S : String;+ N : Integer;+Begin+ Board := PCBServer.GetCurrentPCBBoard;+ If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;+ S := ''; N := 0;+ BIt := Board.BoardIterator_Create;+ BIt.AddFilter_ObjectSet(MkSet(eComponentObject));+ BIt.AddFilter_LayerSet(AllLayers);+ BIt.AddFilter_Method(eProcessAll);+ Comp := BIt.FirstPCBObject;+ While Comp <> Nil Do+ Begin+ If S <> '' Then S := S + ',';+ S := S + '"' + JsonEsc(Comp.Name.Text) + '"';+ N := N + 1;+ Comp := BIt.NextPCBObject;+ End;+ Board.BoardIterator_Destroy(BIt);+ Result := '{' + JRaw('ok','true') + ',' + JRaw('count', IntToStr(N)) + ',' + JRaw('designators', '[' + S + ']') + '}';+End;++{ component_data: designator=<one> or all (limit=N, default 500) }+Function CmdComponentData(Lines : TStringList) : String;+Var+ Board : IPCB_Board;+ BIt : IPCB_BoardIterator;+ Comp : IPCB_Component;+ Des, S : String;+ N, Limit : Integer;+Begin+ Board := PCBServer.GetCurrentPCBBoard;+ If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;+ Des := Param(Lines, 'designator');+ Limit := StrToIntDef(Param(Lines, 'limit'), 500);+ If Des <> '' Then+ Begin+ Comp := FindCompByDes(Board, Des);+ If Comp = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','designator not found: ' + Des) + '}'; Exit; End;+ Result := '{' + JRaw('ok','true') + ',' + JRaw('component', CompJson(Comp)) + '}';+ Exit;+ End;+ S := ''; N := 0;+ BIt := Board.BoardIterator_Create;+ BIt.AddFilter_ObjectSet(MkSet(eComponentObject));+ BIt.AddFilter_LayerSet(AllLayers);+ BIt.AddFilter_Method(eProcessAll);+ Comp := BIt.FirstPCBObject;+ While (Comp <> Nil) And (N < Limit) Do+ Begin+ If S <> '' Then S := S + ',';+ S := S + CompJson(Comp);+ N := N + 1;+ Comp := BIt.NextPCBObject;+ End;+ Board.BoardIterator_Destroy(BIt);+ Result := '{' + JRaw('ok','true') + ',' + JRaw('count', IntToStr(N)) + ',' + JRaw('components', '[' + S + ']') + '}';+End;++{ component_pins: designator=<one> -> every pad with net, position, size, hole }+Function CmdComponentPins(Lines : TStringList) : String;+Var+ Board : IPCB_Board;+ Comp : IPCB_Component;+ GIt : IPCB_GroupIterator;+ Pad : IPCB_Pad;+ Des, S, N : String;+ K : Integer;+Begin+ Board := PCBServer.GetCurrentPCBBoard;+ If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;+ Des := Param(Lines, 'designator');+ If Des = '' Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','designator is required') + '}'; Exit; End;+ Comp := FindCompByDes(Board, Des);+ If Comp = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','designator not found: ' + Des) + '}'; Exit; End;+ S := ''; K := 0;+ GIt := Comp.GroupIterator_Create;+ GIt.AddFilter_ObjectSet(MkSet(ePadObject));+ GIt.AddFilter_LayerSet(AllLayers);+ Pad := GIt.FirstPCBObject;+ While (Pad <> Nil) And (K < 1000) Do+ Begin+ N := '';+ If Pad.Net <> Nil Then N := Pad.Net.Name;+ If S <> '' Then S := S + ',';+ S := S + '{' + JStr('name', Pad.Name) + ',' + JStr('net', N) + ',' ++ JRaw('xMil', IntToStr(Round(CoordToMils(Pad.x)))) + ',' ++ JRaw('yMil', IntToStr(Round(CoordToMils(Pad.y)))) + ',' ++ JRaw('layerId', IntToStr(Pad.Layer)) + ',' ++ JRaw('wMil', IntToStr(Round(CoordToMils(Pad.TopXSize)))) + ',' ++ JRaw('hMil', IntToStr(Round(CoordToMils(Pad.TopYSize)))) + ',' ++ JRaw('holeMil', IntToStr(Round(CoordToMils(Pad.HoleSize)))) + '}';+ K := K + 1;+ Pad := GIt.NextPCBObject;+ End;+ Comp.GroupIterator_Destroy(GIt);+ Result := '{' + JRaw('ok','true') + ',' + JStr('designator', Comp.Name.Text) + ',' + JRaw('count', IntToStr(K)) + ',' + JRaw('pins', '[' + S + ']') + '}';+End;++{ list_nets: every net on the board with its pad count (tallied over all pads) }+Function CmdListNets(Lines : TStringList) : String;+Var+ Board : IPCB_Board;+ BIt : IPCB_BoardIterator;+ Pad : IPCB_Pad;+ SL : TStringList;+ S, N : String;+ I, C : Integer;+Begin+ Board := PCBServer.GetCurrentPCBBoard;+ If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;+ SL := TStringList.Create;+ BIt := Board.BoardIterator_Create;+ BIt.AddFilter_ObjectSet(MkSet(ePadObject));+ BIt.AddFilter_LayerSet(AllLayers);+ BIt.AddFilter_Method(eProcessAll);+ Pad := BIt.FirstPCBObject;+ While Pad <> Nil Do+ Begin+ If Pad.Net <> Nil Then+ Begin+ N := Pad.Net.Name;+ C := StrToIntDef(SL.Values[N], 0);+ SL.Values[N] := IntToStr(C + 1);+ End;+ Pad := BIt.NextPCBObject;+ End;+ Board.BoardIterator_Destroy(BIt);+ S := '';+ For I := 0 To SL.Count - 1 Do+ Begin+ If S <> '' Then S := S + ',';+ S := S + '{' + JStr('name', SL.Names[I]) + ',' + JRaw('pads', SL.Values[SL.Names[I]]) + '}';+ End;+ Result := '{' + JRaw('ok','true') + ',' + JRaw('count', IntToStr(SL.Count)) + ',' + JRaw('nets', '[' + S + ']') + '}';+ SL.Free;+End;++Function LayerJson(Board : IPCB_Board; L : Integer; Kind : String) : String;+Begin+ Result := '{' + JStr('name', Board.LayerStack.LayerObject[L].Name) + ',' ++ JRaw('id', IntToStr(L)) + ',' + JStr('kind', Kind) + ',' ++ JRaw('displayed', JBool(Board.LayerIsDisplayed[L])) + '}';+End;++{ layers: the fixed layer set with names and visibility (stackup detail is a later verb) }+Function CmdLayers(Lines : TStringList) : String;+Var+ Board : IPCB_Board;+ S : String;+Begin+ Board := PCBServer.GetCurrentPCBBoard;+ If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;+ S := LayerJson(Board, eTopLayer, 'signal') + ',' + LayerJson(Board, eBottomLayer, 'signal') + ',' ++ LayerJson(Board, eTopOverlay, 'overlay') + ',' + LayerJson(Board, eBottomOverlay, 'overlay') + ',' ++ LayerJson(Board, eTopPaste, 'paste') + ',' + LayerJson(Board, eBottomPaste, 'paste') + ',' ++ LayerJson(Board, eTopSolder, 'solder') + ',' + LayerJson(Board, eBottomSolder, 'solder') + ',' ++ LayerJson(Board, eKeepOutLayer, 'keepout') + ',' + LayerJson(Board, eMultiLayer, 'multi') + ',' ++ LayerJson(Board, eDrillGuide, 'drill') + ',' + LayerJson(Board, eDrillDrawing, 'drill') + ',' ++ LayerJson(Board, eMechanical1, 'mechanical') + ',' + LayerJson(Board, eMechanical2, 'mechanical') + ',' ++ LayerJson(Board, eMechanical3, 'mechanical') + ',' + LayerJson(Board, eMechanical4, 'mechanical') + ',' ++ LayerJson(Board, eMechanical5, 'mechanical') + ',' + LayerJson(Board, eMechanical6, 'mechanical') + ',' ++ LayerJson(Board, eMechanical7, 'mechanical') + ',' + LayerJson(Board, eMechanical8, 'mechanical') + ',' ++ LayerJson(Board, eMechanical9, 'mechanical') + ',' + LayerJson(Board, eMechanical10, 'mechanical') + ',' ++ LayerJson(Board, eMechanical11, 'mechanical') + ',' + LayerJson(Board, eMechanical12, 'mechanical') + ',' ++ LayerJson(Board, eMechanical13, 'mechanical') + ',' + LayerJson(Board, eMechanical14, 'mechanical') + ',' ++ LayerJson(Board, eMechanical15, 'mechanical') + ',' + LayerJson(Board, eMechanical16, 'mechanical');+ Result := '{' + JRaw('ok','true') + ',' + JRaw('layers', '[' + S + ']') + '}';+End;+++{ ---- Phase 1 batch 2 ---------------------------------------------------- }+{ net_connections: every pad on the board with its net and owner, plus per-net span.+ net=<name> filters to one net. limit caps pads (default 4000). }+Function CmdNetConnections(Lines : TStringList) : String;+Var+ Board : IPCB_Board;+ BIt : IPCB_BoardIterator;+ Pad : IPCB_Pad;+ Want, S, N, Own, Spans, V : String;+ K, Limit, X, Y, I : Integer;+ SL : TStringList;+ P : Integer;+Begin+ Board := PCBServer.GetCurrentPCBBoard;+ If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;+ Want := Param(Lines, 'net');+ Limit := StrToIntDef(Param(Lines, 'limit'), 4000);+ SL := TStringList.Create;+ S := ''; K := 0;+ BIt := Board.BoardIterator_Create;+ BIt.AddFilter_ObjectSet(MkSet(ePadObject));+ BIt.AddFilter_LayerSet(AllLayers);+ BIt.AddFilter_Method(eProcessAll);+ Pad := BIt.FirstPCBObject;+ While (Pad <> Nil) And (K < Limit) Do+ Begin+ If Pad.Net <> Nil Then+ Begin+ N := Pad.Net.Name;+ If (Want = '') Or (UpperCase(N) = UpperCase(Want)) Then+ Begin+ Own := '';+ If Pad.Component <> Nil Then Own := Pad.Component.Name.Text;+ X := Round(CoordToMils(Pad.x)); Y := Round(CoordToMils(Pad.y));+ If S <> '' Then S := S + ',';+ S := S + '{' + JStr('net', N) + ',' + JStr('pad', Own + '.' + Pad.Name) + ',' ++ JRaw('xMil', IntToStr(X)) + ',' + JRaw('yMil', IntToStr(Y)) + '}';+ K := K + 1;+ V := SL.Values[N];+ If V = '' Then SL.Values[N] := IntToStr(X) + '|' + IntToStr(Y) + '|' + IntToStr(X) + '|' + IntToStr(Y) + '|1'+ Else+ Begin+ P := Pos('|', V); Spans := Copy(V, 1, P - 1); V := Copy(V, P + 1, Length(V));+ I := StrToIntDef(Spans, X); If X < I Then I := X; Spans := IntToStr(I);+ P := Pos('|', V); N := Copy(V, 1, P - 1); V := Copy(V, P + 1, Length(V));+ I := StrToIntDef(N, Y); If Y < I Then I := Y; Spans := Spans + '|' + IntToStr(I);+ P := Pos('|', V); N := Copy(V, 1, P - 1); V := Copy(V, P + 1, Length(V));+ I := StrToIntDef(N, X); If X > I Then I := X; Spans := Spans + '|' + IntToStr(I);+ P := Pos('|', V); N := Copy(V, 1, P - 1); V := Copy(V, P + 1, Length(V));+ I := StrToIntDef(N, Y); If Y > I Then I := Y; Spans := Spans + '|' + IntToStr(I);+ I := StrToIntDef(V, 0) + 1; Spans := Spans + '|' + IntToStr(I);+ SL.Values[Pad.Net.Name] := Spans;+ End;+ End;+ End;+ Pad := BIt.NextPCBObject;+ End;+ Board.BoardIterator_Destroy(BIt);+ Spans := '';+ For I := 0 To SL.Count - 1 Do+ Begin+ If Spans <> '' Then Spans := Spans + ',';+ Spans := Spans + '{' + JStr('net', SL.Names[I]) + ',' + JStr('minXminYmaxXmaxYpads', SL.Values[SL.Names[I]]) + '}';+ End;+ SL.Free;+ Result := '{' + JRaw('ok','true') + ',' + JRaw('count', IntToStr(K)) + ',' + JRaw('pads', '[' + S + ']') + ',' + JRaw('nets', '[' + Spans + ']') + '}';+End;++{ schematic_data: components on the CURRENT schematic sheet with pins.+ designator=<one> filters. pins=0 skips pin detail. limit caps components. }+Function CmdSchematicData(Lines : TStringList) : String;+Var+ Doc : ISch_Document;+ Iter : ISch_Iterator;+ PIt : ISch_Iterator;+ Comp : ISch_Component;+ Pin : ISch_Pin;+ Want, S, PS : String;+ N, Limit : Integer;+ WantPins : Boolean;+Begin+ Doc := SchServer.GetCurrentSchDocument;+ If Doc = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current schematic document (open a .SchDoc first)') + '}'; Exit; End;+ Want := Param(Lines, 'designator');+ Limit := StrToIntDef(Param(Lines, 'limit'), 500);+ WantPins := Param(Lines, 'pins') <> '0';+ S := ''; N := 0;+ Iter := Doc.SchIterator_Create;+ Iter.AddFilter_ObjectSet(MkSet(eSchComponent));+ Comp := Iter.FirstSchObject;+ While (Comp <> Nil) And (N < Limit) Do+ Begin+ If (Want = '') Or (UpperCase(Comp.Designator.Text) = UpperCase(Want)) Then+ Begin+ PS := '';+ If WantPins Then+ Begin+ PIt := Comp.SchIterator_Create;+ PIt.AddFilter_ObjectSet(MkSet(ePin));+ Pin := PIt.FirstSchObject;+ While Pin <> Nil Do+ Begin+ If PS <> '' Then PS := PS + ',';+ PS := PS + '{' + JStr('number', Pin.Designator) + ',' + JStr('name', Pin.Name) + ',' ++ JRaw('electrical', IntToStr(Pin.Electrical)) + '}';+ Pin := PIt.NextSchObject;+ End;+ Comp.SchIterator_Destroy(PIt);+ End;+ If S <> '' Then S := S + ',';+ S := S + '{' + JStr('designator', Comp.Designator.Text) + ',' ++ JRaw('partId', IntToStr(Comp.CurrentPartID)) + ',' ++ JStr('libReference', Comp.LibReference) + ',' ++ JStr('comment', Comp.Comment.Text) + ',' ++ JRaw('xMil', IntToStr(Round(CoordToMils(Comp.Location.X)))) + ',' ++ JRaw('yMil', IntToStr(Round(CoordToMils(Comp.Location.Y)))) + ',' ++ JRaw('orientation', IntToStr(Comp.Orientation)) + ',' ++ JRaw('pins', '[' + PS + ']') + '}';+ N := N + 1;+ End;+ Comp := Iter.NextSchObject;+ End;+ Doc.SchIterator_Destroy(Iter);+ Result := '{' + JRaw('ok','true') + ',' + JStr('document', Doc.DocumentName) + ',' + JRaw('count', IntToStr(N)) + ',' + JRaw('components', '[' + S + ']') + '}';+End;++{ copper_layers: the full copper set (top, mid 1-30, planes 1-16, bottom) with names and visibility }+Function CmdCopperLayers(Lines : TStringList) : String;+Var+ Board : IPCB_Board;+ S : String;+Begin+ Board := PCBServer.GetCurrentPCBBoard;+ If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;+ S := LayerJson(Board, eTopLayer, 'signal');+ S := S + ',' + LayerJson(Board, eMidLayer1, 'signal');+ S := S + ',' + LayerJson(Board, eMidLayer2, 'signal');+ S := S + ',' + LayerJson(Board, eMidLayer3, 'signal');+ S := S + ',' + LayerJson(Board, eMidLayer4, 'signal');+ S := S + ',' + LayerJson(Board, eMidLayer5, 'signal');+ S := S + ',' + LayerJson(Board, eMidLayer6, 'signal');+ S := S + ',' + LayerJson(Board, eMidLayer7, 'signal');+ S := S + ',' + LayerJson(Board, eMidLayer8, 'signal');+ S := S + ',' + LayerJson(Board, eMidLayer9, 'signal');+ S := S + ',' + LayerJson(Board, eMidLayer10, 'signal');+ S := S + ',' + LayerJson(Board, eMidLayer11, 'signal');+ S := S + ',' + LayerJson(Board, eMidLayer12, 'signal');+ S := S + ',' + LayerJson(Board, eMidLayer13, 'signal');+ S := S + ',' + LayerJson(Board, eMidLayer14, 'signal');+ S := S + ',' + LayerJson(Board, eMidLayer15, 'signal');+ S := S + ',' + LayerJson(Board, eMidLayer16, 'signal');+ S := S + ',' + LayerJson(Board, eMidLayer17, 'signal');+ S := S + ',' + LayerJson(Board, eMidLayer18, 'signal');+ S := S + ',' + LayerJson(Board, eMidLayer19, 'signal');+ S := S + ',' + LayerJson(Board, eMidLayer20, 'signal');+ S := S + ',' + LayerJson(Board, eMidLayer21, 'signal');+ S := S + ',' + LayerJson(Board, eMidLayer22, 'signal');+ S := S + ',' + LayerJson(Board, eMidLayer23, 'signal');+ S := S + ',' + LayerJson(Board, eMidLayer24, 'signal');+ S := S + ',' + LayerJson(Board, eMidLayer25, 'signal');+ S := S + ',' + LayerJson(Board, eMidLayer26, 'signal');+ S := S + ',' + LayerJson(Board, eMidLayer27, 'signal');+ S := S + ',' + LayerJson(Board, eMidLayer28, 'signal');+ S := S + ',' + LayerJson(Board, eMidLayer29, 'signal');+ S := S + ',' + LayerJson(Board, eMidLayer30, 'signal');+ S := S + ',' + LayerJson(Board, eInternalPlane1, 'plane');+ S := S + ',' + LayerJson(Board, eInternalPlane2, 'plane');+ S := S + ',' + LayerJson(Board, eInternalPlane3, 'plane');+ S := S + ',' + LayerJson(Board, eInternalPlane4, 'plane');+ S := S + ',' + LayerJson(Board, eInternalPlane5, 'plane');+ S := S + ',' + LayerJson(Board, eInternalPlane6, 'plane');+ S := S + ',' + LayerJson(Board, eInternalPlane7, 'plane');+ S := S + ',' + LayerJson(Board, eInternalPlane8, 'plane');+ S := S + ',' + LayerJson(Board, eInternalPlane9, 'plane');+ S := S + ',' + LayerJson(Board, eInternalPlane10, 'plane');+ S := S + ',' + LayerJson(Board, eInternalPlane11, 'plane');+ S := S + ',' + LayerJson(Board, eInternalPlane12, 'plane');+ S := S + ',' + LayerJson(Board, eInternalPlane13, 'plane');+ S := S + ',' + LayerJson(Board, eInternalPlane14, 'plane');+ S := S + ',' + LayerJson(Board, eInternalPlane15, 'plane');+ S := S + ',' + LayerJson(Board, eInternalPlane16, 'plane');+ S := S + ',' + LayerJson(Board, eBottomLayer, 'signal');+ Result := '{' + JRaw('ok','true') + ',' + JRaw('layers', '[' + S + ']') + '}';+End;+++{ ---- Phase 1 batch 3 ---------------------------------------------------- }+{ read_rules: every design rule on the board }+Function CmdReadRules(Lines : TStringList) : String;+Var+ Board : IPCB_Board;+ BIt : IPCB_BoardIterator;+ Rule : IPCB_Rule;+ S : String;+ N : Integer;+Begin+ Board := PCBServer.GetCurrentPCBBoard;+ If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;+ S := ''; N := 0;+ BIt := Board.BoardIterator_Create;+ BIt.AddFilter_ObjectSet(MkSet(eRuleObject));+ BIt.AddFilter_LayerSet(AllLayers);+ BIt.AddFilter_Method(eProcessAll);+ Rule := BIt.FirstPCBObject;+ While Rule <> Nil Do+ Begin+ If S <> '' Then S := S + ',';+ S := S + '{' + JStr('name', Rule.Name) + ',' + JRaw('kind', IntToStr(Rule.RuleKind)) + ',' ++ JRaw('enabled', JBool(Rule.Enabled)) + ',' + JRaw('priority', IntToStr(Rule.Priority)) + ',' ++ JStr('scope1', Rule.Scope1Expression) + ',' + JStr('scope2', Rule.Scope2Expression) + ',' ++ JStr('comment', Rule.Comment) + '}';+ N := N + 1;+ Rule := BIt.NextPCBObject;+ End;+ Board.BoardIterator_Destroy(BIt);+ Result := '{' + JRaw('ok','true') + ',' + JRaw('count', IntToStr(N)) + ',' + JRaw('rules', '[' + S + ']') + '}';+End;++{ set_layer_visibility: layerId=<int> displayed=1|0 }+Function CmdSetLayerVisibility(Lines : TStringList) : String;+Var+ Board : IPCB_Board;+ L : Integer;+ Show : Boolean;+Begin+ Board := PCBServer.GetCurrentPCBBoard;+ If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;+ L := StrToIntDef(Param(Lines, 'layerId'), -1);+ If L < 0 Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','layerId (from altium_layers) is required') + '}'; Exit; End;+ Show := Param(Lines, 'displayed') <> '0';+ Board.LayerIsDisplayed[L] := Show;+ Board.ViewManager_UpdateLayerTabs;+ Board.ViewManager_FullUpdate;+ Result := '{' + JRaw('ok','true') + ',' + JRaw('layerId', IntToStr(L)) + ',' + JStr('name', Board.LayerStack.LayerObject[L].Name) + ',' + JRaw('displayed', JBool(Board.LayerIsDisplayed[L])) + '}';+End;++{ create_net_class: name=<class> nets=<A;B;C> }+Function CmdCreateNetClass(Lines : TStringList) : String;+Var+ Board : IPCB_Board;+ Cls : IPCB_ObjectClass;+ Name, Nets, N : String;+ P, K : Integer;+Begin+ Board := PCBServer.GetCurrentPCBBoard;+ If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;+ Name := Param(Lines, 'name');+ Nets := Param(Lines, 'nets');+ If (Name = '') Or (Nets = '') Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','name and nets (semicolon list) are required') + '}'; Exit; End;+ PCBServer.PreProcess;+ Cls := PCBServer.PCBClassFactory(eClassMemberKind_Net);+ Cls.Name := Name;+ K := 0;+ While Nets <> '' Do+ Begin+ P := Pos(';', Nets);+ If P = 0 Then Begin N := Nets; Nets := ''; End+ Else Begin N := Copy(Nets, 1, P - 1); Nets := Copy(Nets, P + 1, Length(Nets)); End;+ If N <> '' Then Begin Cls.AddMemberByName(N); K := K + 1; End;+ End;+ Board.AddPCBObject(Cls);+ PCBServer.PostProcess;+ Result := '{' + JRaw('ok','true') + ',' + JStr('class', Name) + ',' + JRaw('members', IntToStr(K)) + '}';+End;++{ footprint_primitives: designator=<board component> -> geometry dump (tracks, arcs, pads, texts, others counted) }+Function CmdFootprintPrimitives(Lines : TStringList) : String;+Var+ Board : IPCB_Board;+ Comp : IPCB_Component;+ GIt : IPCB_GroupIterator;+ Prim : IPCB_Primitive;+ Trk : IPCB_Track;+ Arc : IPCB_Arc;+ Pad : IPCB_Pad;+ Txt : IPCB_Text;+ Des, S, Item : String;+ K, Other : Integer;+Begin+ Board := PCBServer.GetCurrentPCBBoard;+ If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;+ Des := Param(Lines, 'designator');+ If Des = '' Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','designator is required') + '}'; Exit; End;+ Comp := FindCompByDes(Board, Des);+ If Comp = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','designator not found: ' + Des) + '}'; Exit; End;+ S := ''; K := 0; Other := 0;+ GIt := Comp.GroupIterator_Create;+ GIt.AddFilter_LayerSet(AllLayers);+ Prim := GIt.FirstPCBObject;+ While (Prim <> Nil) And (K < 2000) Do+ Begin+ Item := '';+ If Prim.ObjectId = eTrackObject Then+ Begin+ Trk := Prim;+ Item := '{' + JStr('kind','track') + ',' + JRaw('layerId', IntToStr(Trk.Layer)) + ',' ++ JRaw('x1', IntToStr(Round(CoordToMils(Trk.x1)))) + ',' + JRaw('y1', IntToStr(Round(CoordToMils(Trk.y1)))) + ',' ++ JRaw('x2', IntToStr(Round(CoordToMils(Trk.x2)))) + ',' + JRaw('y2', IntToStr(Round(CoordToMils(Trk.y2)))) + ',' ++ JRaw('wMil', IntToStr(Round(CoordToMils(Trk.Width)))) + '}';+ End+ Else If Prim.ObjectId = eArcObject Then+ Begin+ Arc := Prim;+ Item := '{' + JStr('kind','arc') + ',' + JRaw('layerId', IntToStr(Arc.Layer)) + ',' ++ JRaw('cx', IntToStr(Round(CoordToMils(Arc.XCenter)))) + ',' + JRaw('cy', IntToStr(Round(CoordToMils(Arc.YCenter)))) + ',' ++ JRaw('rMil', IntToStr(Round(CoordToMils(Arc.Radius)))) + ',' + JRaw('start', IntToStr(Round(Arc.StartAngle))) + ',' ++ JRaw('end', IntToStr(Round(Arc.EndAngle))) + ',' + JRaw('wMil', IntToStr(Round(CoordToMils(Arc.LineWidth)))) + '}';+ End+ Else If Prim.ObjectId = ePadObject Then+ Begin+ Pad := Prim;+ Item := '{' + JStr('kind','pad') + ',' + JStr('name', Pad.Name) + ',' + JRaw('layerId', IntToStr(Pad.Layer)) + ',' ++ JRaw('x', IntToStr(Round(CoordToMils(Pad.x)))) + ',' + JRaw('y', IntToStr(Round(CoordToMils(Pad.y)))) + ',' ++ JRaw('wMil', IntToStr(Round(CoordToMils(Pad.TopXSize)))) + ',' + JRaw('hMil', IntToStr(Round(CoordToMils(Pad.TopYSize)))) + ',' ++ JRaw('holeMil', IntToStr(Round(CoordToMils(Pad.HoleSize)))) + '}';+ End+ Else If Prim.ObjectId = eTextObject Then+ Begin+ Txt := Prim;+ Item := '{' + JStr('kind','text') + ',' + JRaw('layerId', IntToStr(Txt.Layer)) + ',' + JStr('text', Txt.Text) + ',' ++ JRaw('x', IntToStr(Round(CoordToMils(Txt.XLocation)))) + ',' + JRaw('y', IntToStr(Round(CoordToMils(Txt.YLocation)))) + '}';+ End+ Else Other := Other + 1;+ If Item <> '' Then+ Begin+ If S <> '' Then S := S + ',';+ S := S + Item;+ K := K + 1;+ End;+ Prim := GIt.NextPCBObject;+ End;+ Comp.GroupIterator_Destroy(GIt);+ Result := '{' + JRaw('ok','true') + ',' + JStr('designator', Comp.Name.Text) + ',' + JStr('pattern', Comp.Pattern) + ',' ++ JRaw('count', IntToStr(K)) + ',' + JRaw('otherPrimitives', IntToStr(Other)) + ',' + JRaw('primitives', '[' + S + ']') + '}';+End;+ Function Dispatch(Cmd : String; Lines : TStringList; Payload : String) : String; Begin If Cmd = 'ping' Then Result := CmdPing(Lines)@@ -2932,6 +3511,17 @@ Begin Else If Cmd = 'place_on_sch' Then Result := CmdPlaceOnSch(Lines) Else If Cmd = 'place_on_pcb' Then Result := CmdPlaceOnPcb(Lines) Else If Cmd = 'pcb_report' Then Result := CmdPcbReport(Lines)+ Else If Cmd = 'list_designators' Then Result := CmdListDesignators(Lines)+ Else If Cmd = 'component_data' Then Result := CmdComponentData(Lines)+ Else If Cmd = 'component_pins' Then Result := CmdComponentPins(Lines)+ Else If Cmd = 'list_nets' Then Result := CmdListNets(Lines)+ Else If Cmd = 'layers' Then Result := CmdLayers(Lines)+ Else If Cmd = 'net_connections' Then Result := CmdNetConnections(Lines)+ Else If Cmd = 'schematic_data' Then Result := CmdSchematicData(Lines)+ Else If Cmd = 'copper_layers' Then Result := CmdCopperLayers(Lines)+ Else If Cmd = 'read_rules' Then Result := CmdReadRules(Lines)+ Else If Cmd = 'set_layer_visibility' Then Result := CmdSetLayerVisibility(Lines)+ Else If Cmd = 'footprint_primitives' Then Result := CmdFootprintPrimitives(Lines) Else If Cmd = 'build_footprint' Then Result := CmdBuildFootprint(Lines) Else If Cmd = 'build_symbol_modes' Then Result := CmdBuildSymbolModes(Lines) Else If Cmd = 'symbol_mode' Then Result := CmdSymbolMode(Lines)@@ -2956,7 +3546,7 @@ Begin gRunning := False; Result := '{' + JRaw('ok', 'true') + ',' + JStr('note', 'agent loop stopping') + '}'; End- Else Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'unknown command: ' + Cmd) + ',' + JStr('known', 'ping,state,run_process,open,eval,lib_list,lib_select,compile,violations,outjob,create_footprint,create_symbol,install_library,compile_intlib,extract_intlib,set_params,add_body,stop') + '}';+ Else Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'unknown command: ' + Cmd) + ',' + JStr('known', 'ping,state,run_process,open,eval,lib_list,lib_select,compile,violations,outjob,create_footprint,create_symbol,install_library,compile_intlib,extract_intlib,set_params,add_body,stop,list_designators,component_data,component_pins,list_nets,layers,net_connections,schematic_data,copper_layers,read_rules,set_layer_visibility,footprint_primitives') + '}'; End; { ------------------------------------------------------------------ loop --- }
src/bridge.json+13−2@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "altium", "displayName": "Altium Designer",- "version": "0.1.10",+ "version": "0.1.11", "description": "Drive Altium Designer from Adom: a resident DelphiScript agent (eval + the 1,100-command process launcher), the X2 -R command line, UIA over the VCL shell, dialogs by PID, screenshots of every Altium window, and Altium 365.", "homepage": "https://wiki.adom.inc/adom/altium-bridge", "docs": "https://wiki.adom.inc/adom/altium-bridge",@@ -103,7 +103,18 @@ "altium_set_visibility", "altium_xcopy_footprint", "altium_import_part",- "altium_install_library_bundle"+ "altium_install_library_bundle",+ "altium_list_designators",+ "altium_component_data",+ "altium_component_pins",+ "altium_list_nets",+ "altium_layers",+ "altium_net_connections",+ "altium_schematic_data",+ "altium_copper_layers",+ "altium_read_rules",+ "altium_set_layer_visibility",+ "altium_footprint_primitives" ], "statusVerb": "altium_status", "uninstall": "altium_uninstall",
src/handlers/agent.py+49−13@@ -32,6 +32,7 @@ import uuid from pathlib import Path from handlers import altium_detect+import caller_identity _inbox_lock = threading.Lock() _no_recover = False@@ -205,21 +206,56 @@ def _kill_stray_helpers() -> list[int]: return out -def _reap_busy_notice() -> list:+def _reap_busy_notice(wait_sec: float = 4.0) -> list: """A stray -R helper fired at a busy main instance makes Windows raise the single-instance- notice ("There is another instance ... but it is busy at the moment") as a modal on the- USER's screen — owned by the helper X2, not the main one (issue #3 follow-up, Colby). Scan- every X2 dialog and dismiss that specific notice in the background so it never lingers."""- from handlers import dialogs+ notice ("There is another instance of running, but it is busy at the moment") as a modal on+ the USER's screen. Measured 2026-08-31: it can appear a beat AFTER the helper is reaped and+ our own X2-pid enumeration missed it twice. So: ask ab (the desktop authority) — find any+ "Information" window whose text carries the notice, press its OK, and keep looking for a+ short window. Never blind-dismiss anything else.""" reaped = []- try:- for d in dialogs.scan(main_pid_only=False):- blob = ((d.get("body") or "") + " " + (d.get("title") or "")).lower()- if "another instance" in blob or "busy at the moment" in blob or "cannot proceed with the request" in blob:- dialogs.press(d["hwnd"], "OK")- reaped.append(d.get("title") or "notice")- except Exception:- pass+ deadline = time.time() + wait_sec+ seen = set()+ while time.time() < deadline:+ try:+ r = caller_identity.ad_callback("desktop_find_window", {"titleContains": "Information"}, on_behalf=False, timeout=8) or {}+ wins = r.get("matches") or r.get("windows") or ([r.get("best")] if r.get("best") else [])+ except Exception:+ wins = []+ hit = False+ for w in wins:+ hw = (w or {}).get("hwnd")+ if not hw or hw in seen:+ continue+ try:+ t = caller_identity.ad_callback("desktop_find_control", {"hwnd": hw, "contains": "another instance", "role": "text"}, on_behalf=False, timeout=8) or {}+ found = bool(t.get("control") or t.get("matches") or t.get("found"))+ except Exception:+ found = False+ if not found:+ continue+ seen.add(hw)+ try:+ caller_identity.ad_callback("desktop_ui_click", {"hwnd": hw, "name": "OK", "role": "button"}, on_behalf=False, timeout=8)+ reaped.append(hw)+ hit = True+ except Exception:+ pass+ # fallback: our own X2 scan (covers a notice parented to a live helper we still track)+ try:+ from handlers import dialogs+ for d in dialogs.scan(main_pid_only=False):+ blob = ((d.get("body") or "") + " " + (d.get("title") or "")).lower()+ if d["hwnd"] not in seen and ("another instance" in blob or "busy at the moment" in blob or "cannot proceed with the request" in blob):+ dialogs.press(d["hwnd"], "OK"); seen.add(d["hwnd"]); reaped.append(d["hwnd"]); hit = True+ except Exception:+ pass+ if hit:+ time.sleep(0.5)+ continue+ if reaped:+ break+ time.sleep(0.5) return reaped
src/server.py+86−1@@ -1501,6 +1501,91 @@ def _ps(script, timeout=60): return _shell(f"powershell -NoProfile -EncodedCommand {enc}", timeout=timeout) +# ---- Phase 1: board introspection (read-only; parity with coffeenmusic/eda-agent) ----+def _board_read(cmd, args, params=None, timeout=90):+ g = _need_licensed()+ if g:+ return g+ r = agent.call(cmd, params or {}, timeout=float(args.get("timeout", timeout)))+ if not r.get("success", True) and "no current PCB board" in (r.get("error") or ""):+ r["_hint"] = "Open a .PcbDoc first (altium_open_document / altium_show_2d_board), then retry."+ return r+++def h_list_designators(args):+ return _board_read("list_designators", args)+++def h_component_data(args):+ return _board_read("component_data", args, {"designator": args.get("designator", ""), "limit": str(args.get("limit", 500))})+++def h_component_pins(args):+ if not args.get("designator"):+ return {"success": False, "error": "designator is required (e.g. U1)"}+ return _board_read("component_pins", args, {"designator": args["designator"]})+++def h_list_nets(args):+ return _board_read("list_nets", args)+++def h_layers(args):+ return _board_read("layers", args)+++def h_net_connections(args):+ return _board_read("net_connections", args, {"net": args.get("net", ""), "limit": str(args.get("limit", 4000))}, timeout=120)+++def h_schematic_data(args):+ g = _need_licensed()+ if g:+ return g+ r = agent.call("schematic_data", {"designator": args.get("designator", ""), "limit": str(args.get("limit", 500)),+ "pins": "0" if args.get("pins") is False else "1"}, timeout=float(args.get("timeout", 120)))+ if "no current schematic" in (r.get("error") or ""):+ r["_hint"] = "Open a .SchDoc first (altium_open_document / altium_show_schematic), then retry."+ return r+++def h_copper_layers(args):+ return _board_read("copper_layers", args)+++def h_read_rules(args):+ return _board_read("read_rules", args)+++def h_set_layer_visibility(args):+ g = _need_licensed()+ if g:+ return g+ if args.get("layerId") is None:+ return {"success": False, "error": "layerId is required (from altium_layers / altium_copper_layers)"}+ return agent.call("set_layer_visibility", {"layerId": str(args["layerId"]), "displayed": "0" if args.get("displayed") is False else "1"}, timeout=float(args.get("timeout", 60)))+++def h_create_net_class(args):+ g = _need_licensed()+ if g:+ return g+ if not args.get("name") or not args.get("nets"):+ return {"success": False, "error": "name and nets (list or 'A;B;C') are required"}+ nets = args["nets"]+ if isinstance(nets, list):+ nets = ";".join(str(n) for n in nets)+ r = agent.call("create_net_class", {"name": args["name"], "nets": nets}, timeout=float(args.get("timeout", 60)))+ r.setdefault("_hint", "Net class added to the board (unsaved). altium_save to persist; altium_read_rules to scope rules to it.")+ return r+++def h_footprint_primitives(args):+ if not args.get("designator"):+ return {"success": False, "error": "designator is required (a placed component, e.g. U1)"}+ return _board_read("footprint_primitives", args, {"designator": args["designator"]}, timeout=120)++ def _todo(name, phase): def h(args): return {"success": False, "errorCode": "not_implemented", "verb": f"altium_{name}",@@ -1588,7 +1673,7 @@ COMMAND_HANDLERS = { "install_library": h_install_library, "install_library_bundle": h_install_library_bundle, "license_status": h_license_status, "ensure_licensed": h_ensure_licensed, "setup_trial": h_setup_trial, "compile_intlib": h_compile_intlib, "extract_intlib": h_extract_intlib, "set_params": h_set_params, "add_body": h_add_body,- "place_on_sch": h_place_on_sch, "place_on_pcb": h_place_on_pcb, "pcb_report": h_pcb_report, "build_footprint": h_build_footprint, "build_symbol_modes": h_build_symbol_modes, "add_models": h_add_models, "remove_symbol": h_remove_symbol, "clone_symbol": h_clone_symbol, "rename_symbol": h_rename_symbol, "read_symbol": h_read_symbol, "copy_footprint": h_copy_footprint, "rename_footprint": h_rename_footprint, "remove_footprint": h_remove_footprint, "cleanup_libs": h_cleanup_libs, "set_visibility": h_set_visibility, "xcopy_footprint": h_xcopy_footprint, "import_part": h_import_part, "normalize_bodies": h_normalize_bodies, "test_board": h_test_board, "board_shape": h_board_shape, "swap_footprint": h_swap_footprint, "symbol_mode": h_symbol_mode,+ "place_on_sch": h_place_on_sch, "place_on_pcb": h_place_on_pcb, "pcb_report": h_pcb_report, "list_designators": h_list_designators, "component_data": h_component_data, "component_pins": h_component_pins, "list_nets": h_list_nets, "layers": h_layers, "net_connections": h_net_connections, "schematic_data": h_schematic_data, "copper_layers": h_copper_layers, "read_rules": h_read_rules, "set_layer_visibility": h_set_layer_visibility, "footprint_primitives": h_footprint_primitives, "build_footprint": h_build_footprint, "build_symbol_modes": h_build_symbol_modes, "add_models": h_add_models, "remove_symbol": h_remove_symbol, "clone_symbol": h_clone_symbol, "rename_symbol": h_rename_symbol, "read_symbol": h_read_symbol, "copy_footprint": h_copy_footprint, "rename_footprint": h_rename_footprint, "remove_footprint": h_remove_footprint, "cleanup_libs": h_cleanup_libs, "set_visibility": h_set_visibility, "xcopy_footprint": h_xcopy_footprint, "import_part": h_import_part, "normalize_bodies": h_normalize_bodies, "test_board": h_test_board, "board_shape": h_board_shape, "swap_footprint": h_swap_footprint, "symbol_mode": h_symbol_mode, "install_extension": h_install_extension, "extension_ping": h_extension_ping, "export_step": h_export_step, "import_kicad": h_import_kicad, "a365_configure": h_a365_configure, "a365_status": h_a365_status, "a365_query": h_a365_query,