← 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

John Lauer ·a47eeac28e ·1mo ago ·parent 43aced9
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",