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
0.2.13: test_board and place_on_sch accept a ';' list and put every part on ONE board / ONE sheet (John's ToF demo, 2026-09-19); describe entries for both; eval-halt notes
9 files changed
+101−31
CHANGELOG.md+5@@ -1,5 +1,10 @@ # Changelog +## 0.2.13 (2026-09-19)+- **Several parts on ONE document** (John's ToF demo: three VL53Lx sensors on one schematic and one board). `altium_test_board` and `altium_place_on_sch` now accept a `;` list in `footprintName` / `symbolName`: every part lands on the same new board (spread evenly along X inside the wMm x hMm outline) or the same new sheet (U1..Un, 5000 mil apart), and both save to `pcbPath` / `schPath` when given. Single-part calls are unchanged; `assigns`/`stubs` apply to the last part placed. Both verbs finally have `describe` entries. Agent template only: deploy the tmpl beside the exe and `altium_agent_stop` + `_start`; the 0.2.9 exe keeps working with it.+- Measured on ConfRoomROG (ADDevelop): `altium_eval` scripts that call `PCBServer.PreProcess` / `PostProcess` or even `TStringList.SaveToFile` halt before their first statement completes and leave an X2 error modal whose text is not readable (`dialogs[].body` empty); clear with `altium_dismiss_dialogs {Cancel}` + `altium_agent_recover`, twice. adom/altium-bridge#11 asks for readable eval errors and for append-to-existing placement.++ ## 0.2.12 (2026-09-08) - **The one-click install no longer dies at Adom Bridge's 60 s budget** (Colby, live on 2026-09-08: a 7-part Cloud Library export landed in Altium, SCH Library panel and all, while the page said "Could not install: 'altium_install_library_bundle' did not respond within ab's 60s budget"). ab enforces a per-verb budget it reads from `bridge.json`, and `install_library_bundle` was never declared there after 0.2.8 gave it a cold launch and 0.2.9 the open, panel-select and capture finish. Declared now: 330 s for the bundle install (the launch alone is 240), 150 s for `install_library`, and the verb's `describe` entry is marked long-running with `altium_status` as its status verb. The reply carries `phasesMs` (launch, licence, unpack, register, open, list, panel, capture, agentStop) and `totalMs`, so the next slow run names its step instead of leaving a guess. - **The receipt survives a timeout.** The bridge serves one verb at a time and finishes an install ab has already given up on; the receipt, proof frame included, is now kept in memory. `altium_status` carries a light `lastInstall` (with the `installId` the client sent), and the new `altium_install_receipt` returns the full reply. The wiki page waits through this path (git-wiki e1c66a1), so a timed-out install still ends in the success line and the proof shot.
bridge-rs/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "altium-bridge"-version = "0.2.8"+version = "0.2.13" edition = "2021" description = "Adom altium-bridge — the bridge process, in Rust. Calls ab's verbs for the desktop; speaks file IPC to the resident DelphiScript agent inside Altium." license = "MIT"
bridge-rs/src/main.rs+1−1@@ -25,7 +25,7 @@ mod win32; use std::time::Instant; use tiny_http::{Header, Method, Response, Server}; -pub const BRIDGE_VERSION: &str = "0.2.9";+pub const BRIDGE_VERSION: &str = "0.2.13"; fn json_response(status: u16, body: &serde_json::Value) -> Response<std::io::Cursor<Vec<u8>>> { let data = serde_json::to_vec(body).unwrap_or_else(|_| b"{}".to_vec());
bridge-rs/src/verbs.rs+12@@ -101,6 +101,18 @@ fn describe() -> Value { json!({"symbol": "string", "footprint": "string", "steps": "[]"}), 300, "Check the step counts: models:0 fails the call - a symbol with no footprint model places with nothing attached.", &["read_symbol", "show_symbol"], &["add_models APPENDS on every call (not idempotent) - repair is remove_symbol + re-import.", "The first part into a NEW library can silently skip models+params if the bridge restarted mid-flight - verify with read_symbol."], json!({})),+ verb_doc("test_board", "Place one footprint, or a ';' list of footprints, from the open PcbLib on ONE new board (wMm x hMm outline), spread evenly along X; saves to pcbPath when given.",+ json!({"libraryPath": "path (.PcbLib)", "footprintName": "string | 'A;B;C'", "wMm": "number", "hMm": "number", "pcbPath": "path (.PcbDoc, optional save)", "assigns": "des=NET;...", "stubs": "des=x,y;..."}),+ json!({"placed": "string", "count": "int", "pcb": "path", "padsOnComp": "int", "bodiesOnComp": "int"}), 240,+ "Three parts on one board: footprintName 'VL53L5CX;VL53L7CX;VL53L8CX' + pcbPath, then altium_show_3d_board {filePath: pcbPath}. Every call creates a NEW PcbDoc.",+ &["place_on_sch", "show_3d_board", "add_body"], &["assigns/stubs apply to the LAST part placed.", "pcb is empty when pcbPath is omitted: the board exists only unsaved as PCB<n>.PcbDoc."],+ json!({"libraryPath": "C:/lib/Adom_ToF.PcbLib", "footprintName": "VL53L5CX;VL53L7CX;VL53L8CX", "wMm": 39, "hMm": 14, "pcbPath": "C:/proj/tof.PcbDoc"})),+ verb_doc("place_on_sch", "Place one symbol, or a ';' list of symbols, from the open SchLib on ONE new sheet as U1..Un (5000 mil apart); saves to schPath when given.",+ json!({"libraryPath": "path (.SchLib)", "symbolName": "string | 'A;B;C'", "schPath": "path (.SchDoc, optional save)"}),+ json!({"placed": "string", "count": "int", "designator": "string", "primitives": "int", "sch": "path"}), 90,+ "Three parts on one sheet: symbolName 'VL53L5CX;VL53L7CX;VL53L8CX' + schPath, then altium_show_schematic {filePath: schPath}.",+ &["test_board", "show_schematic", "read_schematic"], &["Every call creates a NEW SchDoc; there is no append-to-sheet yet.", "The SchLib must be the current document (the verb opens it)."],+ json!({"libraryPath": "C:/lib/Adom_ToF.SchLib", "symbolName": "VL53L5CX;VL53L7CX;VL53L8CX", "schPath": "C:/proj/tof.SchDoc"})), verb_doc("show_symbol", "Open a SchLib member in the Symbol editor and capture a background proof.", json!({"libraryPath": "path", "symbolName": "string"}), shot.clone(), 60, "Never foregrounds: a minimized Altium is restored INTO THE BACKGROUND and the user's window handed straight back.", &["show_footprint", "show_3d_chip", "show_library"], &["A capture under 400px wide is refused as proof_unavailable - a title-bar sliver is not evidence."], json!({})),
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "altium-bridge",- "version": "0.2.12",+ "version": "0.2.13", "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": [
page.json+1−1@@ -4,7 +4,7 @@ "type": "app", "title": "Altium - the Altium Bridge", "brief": "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.",- "version": "0.2.12",+ "version": "0.2.13", "hero": { "type": "image", "path": "screenshots/hero.png"
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-0.2.12+0.2.13
src/agent/AdomAgent.pas.tmpl+78−25@@ -1335,30 +1335,40 @@ Var Cpy : ISch_GraphicalObject; SD : IServerDocument; Sheet : ISch_Document;- SchPath, LibRef : String;- NCopied : Integer;+ SchPath, LibRef, Missing, Placed : String;+ NCopied, K : Integer;+ RefList : TStringList; Begin SchPath := Param(Lines, 'sch'); LibRef := Param(Lines, 'libref');+ { libref may be a ';' list: every symbol lands on ONE new sheet as U1..Un, 5000 mil apart+ (John, 2026-09-19: three ToF sensors on one schematic for the demo). }+ RefList := SplitSemi(LibRef); SLib := SchServer.GetCurrentSchDocument; If (SLib = Nil) Or (SLib.ObjectID <> eSchLib) Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','open the SchLib first (it must be the current document)') + '}'; Exit; End;- LibCmp := Nil;- Iter := SLib.SchLibIterator_Create;- Iter.AddFilter_ObjectSet(MkSet(eSchComponent));- LibCmp := Iter.FirstSchObject;- While LibCmp <> Nil Do+ Missing := '';+ For K := 0 To RefList.Count - 1 Do Begin- If UpperCase(LibCmp.LibReference) = UpperCase(LibRef) Then Break;- LibCmp := Iter.NextSchObject;+ LibCmp := Nil;+ Iter := SLib.SchLibIterator_Create;+ Iter.AddFilter_ObjectSet(MkSet(eSchComponent));+ LibCmp := Iter.FirstSchObject;+ While LibCmp <> Nil Do+ Begin+ If UpperCase(LibCmp.LibReference) = UpperCase(RefList[K]) Then Break;+ LibCmp := Iter.NextSchObject;+ End;+ SLib.SchIterator_Destroy(Iter);+ If LibCmp = Nil Then Missing := Missing + RefList[K] + ' '; End;- SLib.SchIterator_Destroy(Iter);- If LibCmp = Nil Then+ If (RefList.Count = 0) Or (Missing <> '') Then Begin- Result := '{' + JRaw('ok','false') + ',' + JStr('error','component not found in SchLib: ' + LibRef) + '}';+ Result := '{' + JRaw('ok','false') + ',' + JStr('error','component not found in SchLib: ' + Trim(Missing + LibRef)) + '}';+ RefList.Free; Exit; End; SD := CreateNewDocumentFromDocumentKind('SCH');@@ -1369,6 +1379,20 @@ Begin End; Client.ShowDocument(SD); Sheet := SchServer.GetCurrentSchDocument;+ Placed := '';+ NCopied := 0;+ For K := 0 To RefList.Count - 1 Do+ Begin+ LibCmp := Nil;+ Iter := SLib.SchLibIterator_Create;+ Iter.AddFilter_ObjectSet(MkSet(eSchComponent));+ LibCmp := Iter.FirstSchObject;+ While LibCmp <> Nil Do+ Begin+ If UpperCase(LibCmp.LibReference) = UpperCase(RefList[K]) Then Break;+ LibCmp := Iter.NextSchObject;+ End;+ SLib.SchIterator_Destroy(Iter); { Build a fresh component instance and copy the library part's primitives into it. ISch_Component.Replicate does NOT re-parent the pins/rectangle onto a normal sheet (measured 2026-08-30: only the designator/comment text placed), so copy@@ -1378,8 +1402,7 @@ Begin Inst.DisplayMode := 0; Inst.LibReference := LibCmp.LibReference; Inst.ComponentDescription := LibCmp.ComponentDescription;- Inst.Location := Point(MilsToCoord(5000), MilsToCoord(4000));- NCopied := 0;+ Inst.Location := Point(MilsToCoord(2000 + K * 5000), MilsToCoord(4000)); PIt := LibCmp.SchIterator_Create; Prim := PIt.FirstSchObject; While Prim <> Nil Do@@ -1392,16 +1415,22 @@ Begin Cpy := Prim.Replicate; Cpy.OwnerPartId := 1; Cpy.OwnerPartDisplayMode := 0;+ { copied primitives keep the library coordinates; Inst.Location does not carry them+ (measured 2026-09-19: three parts drew on top of each other), so shift each child }+ If K > 0 Then Cpy.MoveByXY(MilsToCoord(K * 5000), 0); Inst.AddSchObject(Cpy); NCopied := NCopied + 1; End; Prim := PIt.NextSchObject; End; LibCmp.SchIterator_Destroy(PIt);- Inst.Designator.Text := 'U1';+ Inst.Designator.Text := 'U' + IntToStr(K + 1); Inst.Designator.IsHidden := False; Sheet.RegisterSchObjectInContainer(Inst); SchServer.RobotManager.SendMessage(Sheet.I_ObjectAddress, c_BroadCast, SCHM_PrimitiveRegistration, Inst.I_ObjectAddress);+ If Placed <> '' Then Placed := Placed + ';';+ Placed := Placed + RefList[K];+ End; Sheet.GraphicallyInvalidate; ResetParameters; AddStringParameter('Action','All'); RunProcess('Sch:Zoom'); If SchPath <> '' Then@@ -1409,7 +1438,8 @@ Begin SD.SetFileName(SchPath); SD.DoFileSave(''); End;- Result := '{' + JRaw('ok','true') + ',' + JStr('sch', SchPath) + ',' + JStr('placed', LibRef) + ',' + JStr('designator','U1') + ',' + JRaw('primitives', IntToStr(NCopied)) + '}';+ Result := '{' + JRaw('ok','true') + ',' + JStr('sch', SchPath) + ',' + JStr('placed', Placed) + ',' + JStr('designator','U1') + ',' + JRaw('count', IntToStr(RefList.Count)) + ',' + JRaw('primitives', IntToStr(NCopied)) + '}';+ RefList.Free; End; Function CmdPlaceOnPcb(Lines : TStringList) : String;@@ -2383,12 +2413,16 @@ Var GIt : IPCB_GroupIterator; Pad : IPCB_Pad; SL : TStringList;- I, WC, HC, CX, CY, PX, PY, Found : Integer;- FpName, PcbPath, Des, NetName, Item : String;+ FpList : TStringList;+ I, K, WC, HC, CX, CY, PX, PY, Found : Integer;+ FpName, PcbPath, Des, NetName, Item, Missing : String; Ok : Boolean; Begin FpName := Param(Lines, 'footprint'); PcbPath := Param(Lines, 'pcb');+ { footprint may be a ';' list: every part lands on ONE board, spread evenly along X+ (John, 2026-09-19: three ToF sensors on one board for the demo). }+ FpList := SplitSemi(FpName); WC := MMsToCoord(StrToFloat(Param(Lines, 'wMm'))); HC := MMsToCoord(StrToFloat(Param(Lines, 'hMm'))); PLib := PCBServer.GetCurrentPCBLibrary;@@ -2397,15 +2431,21 @@ Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','open the PcbLib first') + '}'; Exit; End;- Ok := False;- For I := 0 To PLib.ComponentCount - 1 Do+ Missing := '';+ For K := 0 To FpList.Count - 1 Do Begin- LibFp := PLib.GetComponent(I);- If UpperCase(LibFp.Name) = UpperCase(FpName) Then Begin Ok := True; Break; End;+ Ok := False;+ For I := 0 To PLib.ComponentCount - 1 Do+ Begin+ LibFp := PLib.GetComponent(I);+ If UpperCase(LibFp.Name) = UpperCase(FpList[K]) Then Begin Ok := True; Break; End;+ End;+ If Not Ok Then Missing := Missing + FpList[K] + ' '; End;- If Not Ok Then+ If (FpList.Count = 0) Or (Missing <> '') Then Begin- Result := '{' + JRaw('ok','false') + ',' + JStr('error','footprint not found: ' + FpName) + '}';+ Result := '{' + JRaw('ok','false') + ',' + JStr('error','footprint not found: ' + Trim(Missing + FpName)) + '}';+ FpList.Free; Exit; End; SD := CreateNewDocumentFromDocumentKind('PCB');@@ -2437,7 +2477,18 @@ Begin BO.Rebuild; BO.Validate; CX := WC Div 2; CY := HC Div 2;- Inst := PlaceCopy(Board, LibFp, CX, CY);+ { one part: centred; N parts: at W*(k+1)/(N+1). Inst keeps the LAST placed part so the+ single-part assigns/stubs behaviour is unchanged. }+ For K := 0 To FpList.Count - 1 Do+ Begin+ For I := 0 To PLib.ComponentCount - 1 Do+ Begin+ LibFp := PLib.GetComponent(I);+ If UpperCase(LibFp.Name) = UpperCase(FpList[K]) Then Break;+ End;+ CX := (WC * (K + 1)) Div (FpList.Count + 1);+ Inst := PlaceCopy(Board, LibFp, CX, CY);+ End; AssignPadNets(Board, Inst, Param(Lines, 'assigns')); { stub tracks: des=x,y (mm from centre) -> a track from that pad to the point, on its net } SL := SplitSemi(Param(Lines, 'stubs'));@@ -2481,8 +2532,10 @@ Begin SD.DoFileSave(''); End; Result := '{' + JRaw('ok','true') + ',' + JStr('pcb', PcbPath) + ',' + JStr('placed', FpName) + ',' ++ JRaw('count', IntToStr(FpList.Count)) + ',' + JRaw('padsOnComp', IntToStr(GroupCount(Inst, ePadObject))) + ',' + JRaw('bodiesOnComp', IntToStr(GroupCount(Inst, eComponentBodyObject))) + '}';+ FpList.Free; End; { Replace the first component on the current board with another library
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "altium", "displayName": "Altium Designer",- "version": "0.2.12",+ "version": "0.2.13", "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",